##// END OF EJS Templates
Merge pull request #8188 from minrk/bigsplit-traitlets...
Thomas Kluyver -
r21004:5ffdabae merge
parent child Browse files
Show More
@@ -0,0 +1,18 b''
1 """
2 Shim to maintain backwards compatibility with old IPython.config imports.
3 """
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
6
7 import sys
8 from warnings import warn
9
10 warn("The `IPython.config` package has been deprecated. "
11 "You should import from traitlets.config instead.")
12
13 from IPython.utils.shimmodule import ShimModule
14
15 # Unconditionally insert the shim into sys.modules so that further import calls
16 # trigger the custom attribute access above
17
18 sys.modules['IPython.config'] = ShimModule(src='IPython.config', mirror='traitlets.config')
@@ -0,0 +1,5 b''
1 # FIXME: import IPython first, to avoid circular imports
2 # this shouldn't be needed after finishing the big split
3 import IPython
4
5 from .traitlets import *
1 NO CONTENT: new file 100644
This diff has been collapsed as it changes many lines, (1874 lines changed) Show them Hide them
@@ -0,0 +1,1874 b''
1 # encoding: utf-8
2 """
3 A lightweight Traits like module.
4
5 This is designed to provide a lightweight, simple, pure Python version of
6 many of the capabilities of enthought.traits. This includes:
7
8 * Validation
9 * Type specification with defaults
10 * Static and dynamic notification
11 * Basic predefined types
12 * An API that is similar to enthought.traits
13
14 We don't support:
15
16 * Delegation
17 * Automatic GUI generation
18 * A full set of trait types. Most importantly, we don't provide container
19 traits (list, dict, tuple) that can trigger notifications if their
20 contents change.
21 * API compatibility with enthought.traits
22
23 There are also some important difference in our design:
24
25 * enthought.traits does not validate default values. We do.
26
27 We choose to create this module because we need these capabilities, but
28 we need them to be pure Python so they work in all Python implementations,
29 including Jython and IronPython.
30
31 Inheritance diagram:
32
33 .. inheritance-diagram:: IPython.utils.traitlets
34 :parts: 3
35 """
36
37 # Copyright (c) IPython Development Team.
38 # Distributed under the terms of the Modified BSD License.
39 #
40 # Adapted from enthought.traits, Copyright (c) Enthought, Inc.,
41 # also under the terms of the Modified BSD License.
42
43 import contextlib
44 import inspect
45 import re
46 import sys
47 import types
48 from types import FunctionType
49 try:
50 from types import ClassType, InstanceType
51 ClassTypes = (ClassType, type)
52 except:
53 ClassTypes = (type,)
54 from warnings import warn
55
56 from IPython.utils import py3compat
57 from IPython.utils import eventful
58 from IPython.utils.getargspec import getargspec
59 from IPython.utils.importstring import import_item
60 from IPython.utils.py3compat import iteritems, string_types
61 from IPython.testing.skipdoctest import skip_doctest
62
63 SequenceTypes = (list, tuple, set, frozenset)
64
65 #-----------------------------------------------------------------------------
66 # Basic classes
67 #-----------------------------------------------------------------------------
68
69
70 class NoDefaultSpecified ( object ): pass
71 NoDefaultSpecified = NoDefaultSpecified()
72
73
74 class Undefined ( object ): pass
75 Undefined = Undefined()
76
77 class TraitError(Exception):
78 pass
79
80 #-----------------------------------------------------------------------------
81 # Utilities
82 #-----------------------------------------------------------------------------
83
84
85 def class_of ( object ):
86 """ Returns a string containing the class name of an object with the
87 correct indefinite article ('a' or 'an') preceding it (e.g., 'an Image',
88 'a PlotValue').
89 """
90 if isinstance( object, py3compat.string_types ):
91 return add_article( object )
92
93 return add_article( object.__class__.__name__ )
94
95
96 def add_article ( name ):
97 """ Returns a string containing the correct indefinite article ('a' or 'an')
98 prefixed to the specified string.
99 """
100 if name[:1].lower() in 'aeiou':
101 return 'an ' + name
102
103 return 'a ' + name
104
105
106 def repr_type(obj):
107 """ Return a string representation of a value and its type for readable
108 error messages.
109 """
110 the_type = type(obj)
111 if (not py3compat.PY3) and the_type is InstanceType:
112 # Old-style class.
113 the_type = obj.__class__
114 msg = '%r %r' % (obj, the_type)
115 return msg
116
117
118 def is_trait(t):
119 """ Returns whether the given value is an instance or subclass of TraitType.
120 """
121 return (isinstance(t, TraitType) or
122 (isinstance(t, type) and issubclass(t, TraitType)))
123
124
125 def parse_notifier_name(name):
126 """Convert the name argument to a list of names.
127
128 Examples
129 --------
130
131 >>> parse_notifier_name('a')
132 ['a']
133 >>> parse_notifier_name(['a','b'])
134 ['a', 'b']
135 >>> parse_notifier_name(None)
136 ['anytrait']
137 """
138 if isinstance(name, string_types):
139 return [name]
140 elif name is None:
141 return ['anytrait']
142 elif isinstance(name, (list, tuple)):
143 for n in name:
144 assert isinstance(n, string_types), "names must be strings"
145 return name
146
147
148 class _SimpleTest:
149 def __init__ ( self, value ): self.value = value
150 def __call__ ( self, test ):
151 return test == self.value
152 def __repr__(self):
153 return "<SimpleTest(%r)" % self.value
154 def __str__(self):
155 return self.__repr__()
156
157
158 def getmembers(object, predicate=None):
159 """A safe version of inspect.getmembers that handles missing attributes.
160
161 This is useful when there are descriptor based attributes that for
162 some reason raise AttributeError even though they exist. This happens
163 in zope.inteface with the __provides__ attribute.
164 """
165 results = []
166 for key in dir(object):
167 try:
168 value = getattr(object, key)
169 except AttributeError:
170 pass
171 else:
172 if not predicate or predicate(value):
173 results.append((key, value))
174 results.sort()
175 return results
176
177 def _validate_link(*tuples):
178 """Validate arguments for traitlet link functions"""
179 for t in tuples:
180 if not len(t) == 2:
181 raise TypeError("Each linked traitlet must be specified as (HasTraits, 'trait_name'), not %r" % t)
182 obj, trait_name = t
183 if not isinstance(obj, HasTraits):
184 raise TypeError("Each object must be HasTraits, not %r" % type(obj))
185 if not trait_name in obj.traits():
186 raise TypeError("%r has no trait %r" % (obj, trait_name))
187
188 @skip_doctest
189 class link(object):
190 """Link traits from different objects together so they remain in sync.
191
192 Parameters
193 ----------
194 *args : pairs of objects/attributes
195
196 Examples
197 --------
198
199 >>> c = link((obj1, 'value'), (obj2, 'value'), (obj3, 'value'))
200 >>> obj1.value = 5 # updates other objects as well
201 """
202 updating = False
203 def __init__(self, *args):
204 if len(args) < 2:
205 raise TypeError('At least two traitlets must be provided.')
206 _validate_link(*args)
207
208 self.objects = {}
209
210 initial = getattr(args[0][0], args[0][1])
211 for obj, attr in args:
212 setattr(obj, attr, initial)
213
214 callback = self._make_closure(obj, attr)
215 obj.on_trait_change(callback, attr)
216 self.objects[(obj, attr)] = callback
217
218 @contextlib.contextmanager
219 def _busy_updating(self):
220 self.updating = True
221 try:
222 yield
223 finally:
224 self.updating = False
225
226 def _make_closure(self, sending_obj, sending_attr):
227 def update(name, old, new):
228 self._update(sending_obj, sending_attr, new)
229 return update
230
231 def _update(self, sending_obj, sending_attr, new):
232 if self.updating:
233 return
234 with self._busy_updating():
235 for obj, attr in self.objects.keys():
236 setattr(obj, attr, new)
237
238 def unlink(self):
239 for key, callback in self.objects.items():
240 (obj, attr) = key
241 obj.on_trait_change(callback, attr, remove=True)
242
243 @skip_doctest
244 class directional_link(object):
245 """Link the trait of a source object with traits of target objects.
246
247 Parameters
248 ----------
249 source : pair of object, name
250 targets : pairs of objects/attributes
251
252 Examples
253 --------
254
255 >>> c = directional_link((src, 'value'), (tgt1, 'value'), (tgt2, 'value'))
256 >>> src.value = 5 # updates target objects
257 >>> tgt1.value = 6 # does not update other objects
258 """
259 updating = False
260
261 def __init__(self, source, *targets):
262 if len(targets) < 1:
263 raise TypeError('At least two traitlets must be provided.')
264 _validate_link(source, *targets)
265 self.source = source
266 self.targets = targets
267
268 # Update current value
269 src_attr_value = getattr(source[0], source[1])
270 for obj, attr in targets:
271 setattr(obj, attr, src_attr_value)
272
273 # Wire
274 self.source[0].on_trait_change(self._update, self.source[1])
275
276 @contextlib.contextmanager
277 def _busy_updating(self):
278 self.updating = True
279 try:
280 yield
281 finally:
282 self.updating = False
283
284 def _update(self, name, old, new):
285 if self.updating:
286 return
287 with self._busy_updating():
288 for obj, attr in self.targets:
289 setattr(obj, attr, new)
290
291 def unlink(self):
292 self.source[0].on_trait_change(self._update, self.source[1], remove=True)
293 self.source = None
294 self.targets = []
295
296 dlink = directional_link
297
298
299 #-----------------------------------------------------------------------------
300 # Base TraitType for all traits
301 #-----------------------------------------------------------------------------
302
303
304 class TraitType(object):
305 """A base class for all trait descriptors.
306
307 Notes
308 -----
309 Our implementation of traits is based on Python's descriptor
310 prototol. This class is the base class for all such descriptors. The
311 only magic we use is a custom metaclass for the main :class:`HasTraits`
312 class that does the following:
313
314 1. Sets the :attr:`name` attribute of every :class:`TraitType`
315 instance in the class dict to the name of the attribute.
316 2. Sets the :attr:`this_class` attribute of every :class:`TraitType`
317 instance in the class dict to the *class* that declared the trait.
318 This is used by the :class:`This` trait to allow subclasses to
319 accept superclasses for :class:`This` values.
320 """
321
322 metadata = {}
323 default_value = Undefined
324 allow_none = False
325 info_text = 'any value'
326
327 def __init__(self, default_value=NoDefaultSpecified, allow_none=None, **metadata):
328 """Create a TraitType.
329 """
330 if default_value is not NoDefaultSpecified:
331 self.default_value = default_value
332 if allow_none is not None:
333 self.allow_none = allow_none
334
335 if 'default' in metadata:
336 # Warn the user that they probably meant default_value.
337 warn(
338 "Parameter 'default' passed to TraitType. "
339 "Did you mean 'default_value'?"
340 )
341
342 if len(metadata) > 0:
343 if len(self.metadata) > 0:
344 self._metadata = self.metadata.copy()
345 self._metadata.update(metadata)
346 else:
347 self._metadata = metadata
348 else:
349 self._metadata = self.metadata
350
351 self.init()
352
353 def init(self):
354 pass
355
356 def get_default_value(self):
357 """Create a new instance of the default value."""
358 return self.default_value
359
360 def instance_init(self):
361 """Part of the initialization which may depends on the underlying
362 HasTraits instance.
363
364 It is typically overloaded for specific trait types.
365
366 This method is called by :meth:`HasTraits.__new__` and in the
367 :meth:`TraitType.instance_init` method of trait types holding
368 other trait types.
369 """
370 pass
371
372 def init_default_value(self, obj):
373 """Instantiate the default value for the trait type.
374
375 This method is called by :meth:`TraitType.set_default_value` in the
376 case a default value is provided at construction time or later when
377 accessing the trait value for the first time in
378 :meth:`HasTraits.__get__`.
379 """
380 value = self.get_default_value()
381 value = self._validate(obj, value)
382 obj._trait_values[self.name] = value
383 return value
384
385 def set_default_value(self, obj):
386 """Set the default value on a per instance basis.
387
388 This method is called by :meth:`HasTraits.__new__` to instantiate and
389 validate the default value. The creation and validation of
390 default values must be delayed until the parent :class:`HasTraits`
391 class has been instantiated.
392 Parameters
393 ----------
394 obj : :class:`HasTraits` instance
395 The parent :class:`HasTraits` instance that has just been
396 created.
397 """
398 # Check for a deferred initializer defined in the same class as the
399 # trait declaration or above.
400 mro = type(obj).mro()
401 meth_name = '_%s_default' % self.name
402 for cls in mro[:mro.index(self.this_class)+1]:
403 if meth_name in cls.__dict__:
404 break
405 else:
406 # We didn't find one. Do static initialization.
407 self.init_default_value(obj)
408 return
409 # Complete the dynamic initialization.
410 obj._trait_dyn_inits[self.name] = meth_name
411
412 def __get__(self, obj, cls=None):
413 """Get the value of the trait by self.name for the instance.
414
415 Default values are instantiated when :meth:`HasTraits.__new__`
416 is called. Thus by the time this method gets called either the
417 default value or a user defined value (they called :meth:`__set__`)
418 is in the :class:`HasTraits` instance.
419 """
420 if obj is None:
421 return self
422 else:
423 try:
424 value = obj._trait_values[self.name]
425 except KeyError:
426 # Check for a dynamic initializer.
427 if self.name in obj._trait_dyn_inits:
428 method = getattr(obj, obj._trait_dyn_inits[self.name])
429 value = method()
430 # FIXME: Do we really validate here?
431 value = self._validate(obj, value)
432 obj._trait_values[self.name] = value
433 return value
434 else:
435 return self.init_default_value(obj)
436 except Exception:
437 # HasTraits should call set_default_value to populate
438 # this. So this should never be reached.
439 raise TraitError('Unexpected error in TraitType: '
440 'default value not set properly')
441 else:
442 return value
443
444 def __set__(self, obj, value):
445 new_value = self._validate(obj, value)
446 try:
447 old_value = obj._trait_values[self.name]
448 except KeyError:
449 old_value = Undefined
450
451 obj._trait_values[self.name] = new_value
452 try:
453 silent = bool(old_value == new_value)
454 except:
455 # if there is an error in comparing, default to notify
456 silent = False
457 if silent is not True:
458 # we explicitly compare silent to True just in case the equality
459 # comparison above returns something other than True/False
460 obj._notify_trait(self.name, old_value, new_value)
461
462 def _validate(self, obj, value):
463 if value is None and self.allow_none:
464 return value
465 if hasattr(self, 'validate'):
466 value = self.validate(obj, value)
467 if obj._cross_validation_lock is False:
468 value = self._cross_validate(obj, value)
469 return value
470
471 def _cross_validate(self, obj, value):
472 if hasattr(obj, '_%s_validate' % self.name):
473 cross_validate = getattr(obj, '_%s_validate' % self.name)
474 value = cross_validate(value, self)
475 return value
476
477 def __or__(self, other):
478 if isinstance(other, Union):
479 return Union([self] + other.trait_types)
480 else:
481 return Union([self, other])
482
483 def info(self):
484 return self.info_text
485
486 def error(self, obj, value):
487 if obj is not None:
488 e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
489 % (self.name, class_of(obj),
490 self.info(), repr_type(value))
491 else:
492 e = "The '%s' trait must be %s, but a value of %r was specified." \
493 % (self.name, self.info(), repr_type(value))
494 raise TraitError(e)
495
496 def get_metadata(self, key, default=None):
497 return getattr(self, '_metadata', {}).get(key, default)
498
499 def set_metadata(self, key, value):
500 getattr(self, '_metadata', {})[key] = value
501
502
503 #-----------------------------------------------------------------------------
504 # The HasTraits implementation
505 #-----------------------------------------------------------------------------
506
507
508 class MetaHasTraits(type):
509 """A metaclass for HasTraits.
510
511 This metaclass makes sure that any TraitType class attributes are
512 instantiated and sets their name attribute.
513 """
514
515 def __new__(mcls, name, bases, classdict):
516 """Create the HasTraits class.
517
518 This instantiates all TraitTypes in the class dict and sets their
519 :attr:`name` attribute.
520 """
521 # print "MetaHasTraitlets (mcls, name): ", mcls, name
522 # print "MetaHasTraitlets (bases): ", bases
523 # print "MetaHasTraitlets (classdict): ", classdict
524 for k,v in iteritems(classdict):
525 if isinstance(v, TraitType):
526 v.name = k
527 elif inspect.isclass(v):
528 if issubclass(v, TraitType):
529 vinst = v()
530 vinst.name = k
531 classdict[k] = vinst
532 return super(MetaHasTraits, mcls).__new__(mcls, name, bases, classdict)
533
534 def __init__(cls, name, bases, classdict):
535 """Finish initializing the HasTraits class.
536
537 This sets the :attr:`this_class` attribute of each TraitType in the
538 class dict to the newly created class ``cls``.
539 """
540 for k, v in iteritems(classdict):
541 if isinstance(v, TraitType):
542 v.this_class = cls
543 super(MetaHasTraits, cls).__init__(name, bases, classdict)
544
545
546 class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)):
547
548 def __new__(cls, *args, **kw):
549 # This is needed because object.__new__ only accepts
550 # the cls argument.
551 new_meth = super(HasTraits, cls).__new__
552 if new_meth is object.__new__:
553 inst = new_meth(cls)
554 else:
555 inst = new_meth(cls, **kw)
556 inst._trait_values = {}
557 inst._trait_notifiers = {}
558 inst._trait_dyn_inits = {}
559 inst._cross_validation_lock = True
560 # Here we tell all the TraitType instances to set their default
561 # values on the instance.
562 for key in dir(cls):
563 # Some descriptors raise AttributeError like zope.interface's
564 # __provides__ attributes even though they exist. This causes
565 # AttributeErrors even though they are listed in dir(cls).
566 try:
567 value = getattr(cls, key)
568 except AttributeError:
569 pass
570 else:
571 if isinstance(value, TraitType):
572 value.instance_init()
573 if key not in kw:
574 value.set_default_value(inst)
575 inst._cross_validation_lock = False
576 return inst
577
578 def __init__(self, *args, **kw):
579 # Allow trait values to be set using keyword arguments.
580 # We need to use setattr for this to trigger validation and
581 # notifications.
582 with self.hold_trait_notifications():
583 for key, value in iteritems(kw):
584 setattr(self, key, value)
585
586 @contextlib.contextmanager
587 def hold_trait_notifications(self):
588 """Context manager for bundling trait change notifications and cross
589 validation.
590
591 Use this when doing multiple trait assignments (init, config), to avoid
592 race conditions in trait notifiers requesting other trait values.
593 All trait notifications will fire after all values have been assigned.
594 """
595 if self._cross_validation_lock is True:
596 yield
597 return
598 else:
599 self._cross_validation_lock = True
600 cache = {}
601 notifications = {}
602 _notify_trait = self._notify_trait
603
604 def cache_values(*a):
605 cache[a[0]] = a
606
607 def hold_notifications(*a):
608 notifications[a[0]] = a
609
610 self._notify_trait = cache_values
611
612 try:
613 yield
614 finally:
615 try:
616 self._notify_trait = hold_notifications
617 for name in cache:
618 if hasattr(self, '_%s_validate' % name):
619 cross_validate = getattr(self, '_%s_validate' % name)
620 setattr(self, name, cross_validate(getattr(self, name), self))
621 except TraitError as e:
622 self._notify_trait = lambda *x: None
623 for name in cache:
624 if cache[name][1] is not Undefined:
625 setattr(self, name, cache[name][1])
626 else:
627 delattr(self, name)
628 cache = {}
629 notifications = {}
630 raise e
631 finally:
632 self._notify_trait = _notify_trait
633 self._cross_validation_lock = False
634 if isinstance(_notify_trait, types.MethodType):
635 # FIXME: remove when support is bumped to 3.4.
636 # when original method is restored,
637 # remove the redundant value from __dict__
638 # (only used to preserve pickleability on Python < 3.4)
639 self.__dict__.pop('_notify_trait', None)
640 # trigger delayed notifications
641 for v in dict(cache, **notifications).values():
642 self._notify_trait(*v)
643
644 def _notify_trait(self, name, old_value, new_value):
645
646 # First dynamic ones
647 callables = []
648 callables.extend(self._trait_notifiers.get(name,[]))
649 callables.extend(self._trait_notifiers.get('anytrait',[]))
650
651 # Now static ones
652 try:
653 cb = getattr(self, '_%s_changed' % name)
654 except:
655 pass
656 else:
657 callables.append(cb)
658
659 # Call them all now
660 for c in callables:
661 # Traits catches and logs errors here. I allow them to raise
662 if callable(c):
663 argspec = getargspec(c)
664
665 nargs = len(argspec[0])
666 # Bound methods have an additional 'self' argument
667 # I don't know how to treat unbound methods, but they
668 # can't really be used for callbacks.
669 if isinstance(c, types.MethodType):
670 offset = -1
671 else:
672 offset = 0
673 if nargs + offset == 0:
674 c()
675 elif nargs + offset == 1:
676 c(name)
677 elif nargs + offset == 2:
678 c(name, new_value)
679 elif nargs + offset == 3:
680 c(name, old_value, new_value)
681 else:
682 raise TraitError('a trait changed callback '
683 'must have 0-3 arguments.')
684 else:
685 raise TraitError('a trait changed callback '
686 'must be callable.')
687
688
689 def _add_notifiers(self, handler, name):
690 if name not in self._trait_notifiers:
691 nlist = []
692 self._trait_notifiers[name] = nlist
693 else:
694 nlist = self._trait_notifiers[name]
695 if handler not in nlist:
696 nlist.append(handler)
697
698 def _remove_notifiers(self, handler, name):
699 if name in self._trait_notifiers:
700 nlist = self._trait_notifiers[name]
701 try:
702 index = nlist.index(handler)
703 except ValueError:
704 pass
705 else:
706 del nlist[index]
707
708 def on_trait_change(self, handler, name=None, remove=False):
709 """Setup a handler to be called when a trait changes.
710
711 This is used to setup dynamic notifications of trait changes.
712
713 Static handlers can be created by creating methods on a HasTraits
714 subclass with the naming convention '_[traitname]_changed'. Thus,
715 to create static handler for the trait 'a', create the method
716 _a_changed(self, name, old, new) (fewer arguments can be used, see
717 below).
718
719 Parameters
720 ----------
721 handler : callable
722 A callable that is called when a trait changes. Its
723 signature can be handler(), handler(name), handler(name, new)
724 or handler(name, old, new).
725 name : list, str, None
726 If None, the handler will apply to all traits. If a list
727 of str, handler will apply to all names in the list. If a
728 str, the handler will apply just to that name.
729 remove : bool
730 If False (the default), then install the handler. If True
731 then unintall it.
732 """
733 if remove:
734 names = parse_notifier_name(name)
735 for n in names:
736 self._remove_notifiers(handler, n)
737 else:
738 names = parse_notifier_name(name)
739 for n in names:
740 self._add_notifiers(handler, n)
741
742 @classmethod
743 def class_trait_names(cls, **metadata):
744 """Get a list of all the names of this class' traits.
745
746 This method is just like the :meth:`trait_names` method,
747 but is unbound.
748 """
749 return cls.class_traits(**metadata).keys()
750
751 @classmethod
752 def class_traits(cls, **metadata):
753 """Get a `dict` of all the traits of this class. The dictionary
754 is keyed on the name and the values are the TraitType objects.
755
756 This method is just like the :meth:`traits` method, but is unbound.
757
758 The TraitTypes returned don't know anything about the values
759 that the various HasTrait's instances are holding.
760
761 The metadata kwargs allow functions to be passed in which
762 filter traits based on metadata values. The functions should
763 take a single value as an argument and return a boolean. If
764 any function returns False, then the trait is not included in
765 the output. This does not allow for any simple way of
766 testing that a metadata name exists and has any
767 value because get_metadata returns None if a metadata key
768 doesn't exist.
769 """
770 traits = dict([memb for memb in getmembers(cls) if
771 isinstance(memb[1], TraitType)])
772
773 if len(metadata) == 0:
774 return traits
775
776 for meta_name, meta_eval in metadata.items():
777 if type(meta_eval) is not FunctionType:
778 metadata[meta_name] = _SimpleTest(meta_eval)
779
780 result = {}
781 for name, trait in traits.items():
782 for meta_name, meta_eval in metadata.items():
783 if not meta_eval(trait.get_metadata(meta_name)):
784 break
785 else:
786 result[name] = trait
787
788 return result
789
790 def trait_names(self, **metadata):
791 """Get a list of all the names of this class' traits."""
792 return self.traits(**metadata).keys()
793
794 def traits(self, **metadata):
795 """Get a `dict` of all the traits of this class. The dictionary
796 is keyed on the name and the values are the TraitType objects.
797
798 The TraitTypes returned don't know anything about the values
799 that the various HasTrait's instances are holding.
800
801 The metadata kwargs allow functions to be passed in which
802 filter traits based on metadata values. The functions should
803 take a single value as an argument and return a boolean. If
804 any function returns False, then the trait is not included in
805 the output. This does not allow for any simple way of
806 testing that a metadata name exists and has any
807 value because get_metadata returns None if a metadata key
808 doesn't exist.
809 """
810 traits = dict([memb for memb in getmembers(self.__class__) if
811 isinstance(memb[1], TraitType)])
812
813 if len(metadata) == 0:
814 return traits
815
816 for meta_name, meta_eval in metadata.items():
817 if type(meta_eval) is not FunctionType:
818 metadata[meta_name] = _SimpleTest(meta_eval)
819
820 result = {}
821 for name, trait in traits.items():
822 for meta_name, meta_eval in metadata.items():
823 if not meta_eval(trait.get_metadata(meta_name)):
824 break
825 else:
826 result[name] = trait
827
828 return result
829
830 def trait_metadata(self, traitname, key, default=None):
831 """Get metadata values for trait by key."""
832 try:
833 trait = getattr(self.__class__, traitname)
834 except AttributeError:
835 raise TraitError("Class %s does not have a trait named %s" %
836 (self.__class__.__name__, traitname))
837 else:
838 return trait.get_metadata(key, default)
839
840 def add_trait(self, traitname, trait):
841 """Dynamically add a trait attribute to the HasTraits instance."""
842 self.__class__ = type(self.__class__.__name__, (self.__class__,),
843 {traitname: trait})
844 trait.set_default_value(self)
845
846 #-----------------------------------------------------------------------------
847 # Actual TraitTypes implementations/subclasses
848 #-----------------------------------------------------------------------------
849
850 #-----------------------------------------------------------------------------
851 # TraitTypes subclasses for handling classes and instances of classes
852 #-----------------------------------------------------------------------------
853
854
855 class ClassBasedTraitType(TraitType):
856 """
857 A trait with error reporting and string -> type resolution for Type,
858 Instance and This.
859 """
860
861 def _resolve_string(self, string):
862 """
863 Resolve a string supplied for a type into an actual object.
864 """
865 return import_item(string)
866
867 def error(self, obj, value):
868 kind = type(value)
869 if (not py3compat.PY3) and kind is InstanceType:
870 msg = 'class %s' % value.__class__.__name__
871 else:
872 msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) )
873
874 if obj is not None:
875 e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
876 % (self.name, class_of(obj),
877 self.info(), msg)
878 else:
879 e = "The '%s' trait must be %s, but a value of %r was specified." \
880 % (self.name, self.info(), msg)
881
882 raise TraitError(e)
883
884
885 class Type(ClassBasedTraitType):
886 """A trait whose value must be a subclass of a specified class."""
887
888 def __init__ (self, default_value=None, klass=None, allow_none=False,
889 **metadata):
890 """Construct a Type trait
891
892 A Type trait specifies that its values must be subclasses of
893 a particular class.
894
895 If only ``default_value`` is given, it is used for the ``klass`` as
896 well.
897
898 Parameters
899 ----------
900 default_value : class, str or None
901 The default value must be a subclass of klass. If an str,
902 the str must be a fully specified class name, like 'foo.bar.Bah'.
903 The string is resolved into real class, when the parent
904 :class:`HasTraits` class is instantiated.
905 klass : class, str, None
906 Values of this trait must be a subclass of klass. The klass
907 may be specified in a string like: 'foo.bar.MyClass'.
908 The string is resolved into real class, when the parent
909 :class:`HasTraits` class is instantiated.
910 allow_none : bool [ default True ]
911 Indicates whether None is allowed as an assignable value. Even if
912 ``False``, the default value may be ``None``.
913 """
914 if default_value is None:
915 if klass is None:
916 klass = object
917 elif klass is None:
918 klass = default_value
919
920 if not (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)):
921 raise TraitError("A Type trait must specify a class.")
922
923 self.klass = klass
924
925 super(Type, self).__init__(default_value, allow_none=allow_none, **metadata)
926
927 def validate(self, obj, value):
928 """Validates that the value is a valid object instance."""
929 if isinstance(value, py3compat.string_types):
930 try:
931 value = self._resolve_string(value)
932 except ImportError:
933 raise TraitError("The '%s' trait of %s instance must be a type, but "
934 "%r could not be imported" % (self.name, obj, value))
935 try:
936 if issubclass(value, self.klass):
937 return value
938 except:
939 pass
940
941 self.error(obj, value)
942
943 def info(self):
944 """ Returns a description of the trait."""
945 if isinstance(self.klass, py3compat.string_types):
946 klass = self.klass
947 else:
948 klass = self.klass.__name__
949 result = 'a subclass of ' + klass
950 if self.allow_none:
951 return result + ' or None'
952 return result
953
954 def instance_init(self):
955 self._resolve_classes()
956 super(Type, self).instance_init()
957
958 def _resolve_classes(self):
959 if isinstance(self.klass, py3compat.string_types):
960 self.klass = self._resolve_string(self.klass)
961 if isinstance(self.default_value, py3compat.string_types):
962 self.default_value = self._resolve_string(self.default_value)
963
964 def get_default_value(self):
965 return self.default_value
966
967
968 class DefaultValueGenerator(object):
969 """A class for generating new default value instances."""
970
971 def __init__(self, *args, **kw):
972 self.args = args
973 self.kw = kw
974
975 def generate(self, klass):
976 return klass(*self.args, **self.kw)
977
978
979 class Instance(ClassBasedTraitType):
980 """A trait whose value must be an instance of a specified class.
981
982 The value can also be an instance of a subclass of the specified class.
983
984 Subclasses can declare default classes by overriding the klass attribute
985 """
986
987 klass = None
988
989 def __init__(self, klass=None, args=None, kw=None, allow_none=False,
990 **metadata ):
991 """Construct an Instance trait.
992
993 This trait allows values that are instances of a particular
994 class or its subclasses. Our implementation is quite different
995 from that of enthough.traits as we don't allow instances to be used
996 for klass and we handle the ``args`` and ``kw`` arguments differently.
997
998 Parameters
999 ----------
1000 klass : class, str
1001 The class that forms the basis for the trait. Class names
1002 can also be specified as strings, like 'foo.bar.Bar'.
1003 args : tuple
1004 Positional arguments for generating the default value.
1005 kw : dict
1006 Keyword arguments for generating the default value.
1007 allow_none : bool [default True]
1008 Indicates whether None is allowed as a value.
1009
1010 Notes
1011 -----
1012 If both ``args`` and ``kw`` are None, then the default value is None.
1013 If ``args`` is a tuple and ``kw`` is a dict, then the default is
1014 created as ``klass(*args, **kw)``. If exactly one of ``args`` or ``kw`` is
1015 None, the None is replaced by ``()`` or ``{}``, respectively.
1016 """
1017 if klass is None:
1018 klass = self.klass
1019
1020 if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)):
1021 self.klass = klass
1022 else:
1023 raise TraitError('The klass attribute must be a class'
1024 ' not: %r' % klass)
1025
1026 # self.klass is a class, so handle default_value
1027 if args is None and kw is None:
1028 default_value = None
1029 else:
1030 if args is None:
1031 # kw is not None
1032 args = ()
1033 elif kw is None:
1034 # args is not None
1035 kw = {}
1036
1037 if not isinstance(kw, dict):
1038 raise TraitError("The 'kw' argument must be a dict or None.")
1039 if not isinstance(args, tuple):
1040 raise TraitError("The 'args' argument must be a tuple or None.")
1041
1042 default_value = DefaultValueGenerator(*args, **kw)
1043
1044 super(Instance, self).__init__(default_value, allow_none=allow_none, **metadata)
1045
1046 def validate(self, obj, value):
1047 if isinstance(value, self.klass):
1048 return value
1049 else:
1050 self.error(obj, value)
1051
1052 def info(self):
1053 if isinstance(self.klass, py3compat.string_types):
1054 klass = self.klass
1055 else:
1056 klass = self.klass.__name__
1057 result = class_of(klass)
1058 if self.allow_none:
1059 return result + ' or None'
1060
1061 return result
1062
1063 def instance_init(self):
1064 self._resolve_classes()
1065 super(Instance, self).instance_init()
1066
1067 def _resolve_classes(self):
1068 if isinstance(self.klass, py3compat.string_types):
1069 self.klass = self._resolve_string(self.klass)
1070
1071 def get_default_value(self):
1072 """Instantiate a default value instance.
1073
1074 This is called when the containing HasTraits classes'
1075 :meth:`__new__` method is called to ensure that a unique instance
1076 is created for each HasTraits instance.
1077 """
1078 dv = self.default_value
1079 if isinstance(dv, DefaultValueGenerator):
1080 return dv.generate(self.klass)
1081 else:
1082 return dv
1083
1084
1085 class ForwardDeclaredMixin(object):
1086 """
1087 Mixin for forward-declared versions of Instance and Type.
1088 """
1089 def _resolve_string(self, string):
1090 """
1091 Find the specified class name by looking for it in the module in which
1092 our this_class attribute was defined.
1093 """
1094 modname = self.this_class.__module__
1095 return import_item('.'.join([modname, string]))
1096
1097
1098 class ForwardDeclaredType(ForwardDeclaredMixin, Type):
1099 """
1100 Forward-declared version of Type.
1101 """
1102 pass
1103
1104
1105 class ForwardDeclaredInstance(ForwardDeclaredMixin, Instance):
1106 """
1107 Forward-declared version of Instance.
1108 """
1109 pass
1110
1111
1112 class This(ClassBasedTraitType):
1113 """A trait for instances of the class containing this trait.
1114
1115 Because how how and when class bodies are executed, the ``This``
1116 trait can only have a default value of None. This, and because we
1117 always validate default values, ``allow_none`` is *always* true.
1118 """
1119
1120 info_text = 'an instance of the same type as the receiver or None'
1121
1122 def __init__(self, **metadata):
1123 super(This, self).__init__(None, **metadata)
1124
1125 def validate(self, obj, value):
1126 # What if value is a superclass of obj.__class__? This is
1127 # complicated if it was the superclass that defined the This
1128 # trait.
1129 if isinstance(value, self.this_class) or (value is None):
1130 return value
1131 else:
1132 self.error(obj, value)
1133
1134
1135 class Union(TraitType):
1136 """A trait type representing a Union type."""
1137
1138 def __init__(self, trait_types, **metadata):
1139 """Construct a Union trait.
1140
1141 This trait allows values that are allowed by at least one of the
1142 specified trait types. A Union traitlet cannot have metadata on
1143 its own, besides the metadata of the listed types.
1144
1145 Parameters
1146 ----------
1147 trait_types: sequence
1148 The list of trait types of length at least 1.
1149
1150 Notes
1151 -----
1152 Union([Float(), Bool(), Int()]) attempts to validate the provided values
1153 with the validation function of Float, then Bool, and finally Int.
1154 """
1155 self.trait_types = trait_types
1156 self.info_text = " or ".join([tt.info_text for tt in self.trait_types])
1157 self.default_value = self.trait_types[0].get_default_value()
1158 super(Union, self).__init__(**metadata)
1159
1160 def instance_init(self):
1161 for trait_type in self.trait_types:
1162 trait_type.name = self.name
1163 trait_type.this_class = self.this_class
1164 trait_type.instance_init()
1165 super(Union, self).instance_init()
1166
1167 def validate(self, obj, value):
1168 for trait_type in self.trait_types:
1169 try:
1170 v = trait_type._validate(obj, value)
1171 self._metadata = trait_type._metadata
1172 return v
1173 except TraitError:
1174 continue
1175 self.error(obj, value)
1176
1177 def __or__(self, other):
1178 if isinstance(other, Union):
1179 return Union(self.trait_types + other.trait_types)
1180 else:
1181 return Union(self.trait_types + [other])
1182
1183 #-----------------------------------------------------------------------------
1184 # Basic TraitTypes implementations/subclasses
1185 #-----------------------------------------------------------------------------
1186
1187
1188 class Any(TraitType):
1189 default_value = None
1190 info_text = 'any value'
1191
1192
1193 class Int(TraitType):
1194 """An int trait."""
1195
1196 default_value = 0
1197 info_text = 'an int'
1198
1199 def validate(self, obj, value):
1200 if isinstance(value, int):
1201 return value
1202 self.error(obj, value)
1203
1204 class CInt(Int):
1205 """A casting version of the int trait."""
1206
1207 def validate(self, obj, value):
1208 try:
1209 return int(value)
1210 except:
1211 self.error(obj, value)
1212
1213 if py3compat.PY3:
1214 Long, CLong = Int, CInt
1215 Integer = Int
1216 else:
1217 class Long(TraitType):
1218 """A long integer trait."""
1219
1220 default_value = 0
1221 info_text = 'a long'
1222
1223 def validate(self, obj, value):
1224 if isinstance(value, long):
1225 return value
1226 if isinstance(value, int):
1227 return long(value)
1228 self.error(obj, value)
1229
1230
1231 class CLong(Long):
1232 """A casting version of the long integer trait."""
1233
1234 def validate(self, obj, value):
1235 try:
1236 return long(value)
1237 except:
1238 self.error(obj, value)
1239
1240 class Integer(TraitType):
1241 """An integer trait.
1242
1243 Longs that are unnecessary (<= sys.maxint) are cast to ints."""
1244
1245 default_value = 0
1246 info_text = 'an integer'
1247
1248 def validate(self, obj, value):
1249 if isinstance(value, int):
1250 return value
1251 if isinstance(value, long):
1252 # downcast longs that fit in int:
1253 # note that int(n > sys.maxint) returns a long, so
1254 # we don't need a condition on this cast
1255 return int(value)
1256 if sys.platform == "cli":
1257 from System import Int64
1258 if isinstance(value, Int64):
1259 return int(value)
1260 self.error(obj, value)
1261
1262
1263 class Float(TraitType):
1264 """A float trait."""
1265
1266 default_value = 0.0
1267 info_text = 'a float'
1268
1269 def validate(self, obj, value):
1270 if isinstance(value, float):
1271 return value
1272 if isinstance(value, int):
1273 return float(value)
1274 self.error(obj, value)
1275
1276
1277 class CFloat(Float):
1278 """A casting version of the float trait."""
1279
1280 def validate(self, obj, value):
1281 try:
1282 return float(value)
1283 except:
1284 self.error(obj, value)
1285
1286 class Complex(TraitType):
1287 """A trait for complex numbers."""
1288
1289 default_value = 0.0 + 0.0j
1290 info_text = 'a complex number'
1291
1292 def validate(self, obj, value):
1293 if isinstance(value, complex):
1294 return value
1295 if isinstance(value, (float, int)):
1296 return complex(value)
1297 self.error(obj, value)
1298
1299
1300 class CComplex(Complex):
1301 """A casting version of the complex number trait."""
1302
1303 def validate (self, obj, value):
1304 try:
1305 return complex(value)
1306 except:
1307 self.error(obj, value)
1308
1309 # We should always be explicit about whether we're using bytes or unicode, both
1310 # for Python 3 conversion and for reliable unicode behaviour on Python 2. So
1311 # we don't have a Str type.
1312 class Bytes(TraitType):
1313 """A trait for byte strings."""
1314
1315 default_value = b''
1316 info_text = 'a bytes object'
1317
1318 def validate(self, obj, value):
1319 if isinstance(value, bytes):
1320 return value
1321 self.error(obj, value)
1322
1323
1324 class CBytes(Bytes):
1325 """A casting version of the byte string trait."""
1326
1327 def validate(self, obj, value):
1328 try:
1329 return bytes(value)
1330 except:
1331 self.error(obj, value)
1332
1333
1334 class Unicode(TraitType):
1335 """A trait for unicode strings."""
1336
1337 default_value = u''
1338 info_text = 'a unicode string'
1339
1340 def validate(self, obj, value):
1341 if isinstance(value, py3compat.unicode_type):
1342 return value
1343 if isinstance(value, bytes):
1344 try:
1345 return value.decode('ascii', 'strict')
1346 except UnicodeDecodeError:
1347 msg = "Could not decode {!r} for unicode trait '{}' of {} instance."
1348 raise TraitError(msg.format(value, self.name, class_of(obj)))
1349 self.error(obj, value)
1350
1351
1352 class CUnicode(Unicode):
1353 """A casting version of the unicode trait."""
1354
1355 def validate(self, obj, value):
1356 try:
1357 return py3compat.unicode_type(value)
1358 except:
1359 self.error(obj, value)
1360
1361
1362 class ObjectName(TraitType):
1363 """A string holding a valid object name in this version of Python.
1364
1365 This does not check that the name exists in any scope."""
1366 info_text = "a valid object identifier in Python"
1367
1368 if py3compat.PY3:
1369 # Python 3:
1370 coerce_str = staticmethod(lambda _,s: s)
1371
1372 else:
1373 # Python 2:
1374 def coerce_str(self, obj, value):
1375 "In Python 2, coerce ascii-only unicode to str"
1376 if isinstance(value, unicode):
1377 try:
1378 return str(value)
1379 except UnicodeEncodeError:
1380 self.error(obj, value)
1381 return value
1382
1383 def validate(self, obj, value):
1384 value = self.coerce_str(obj, value)
1385
1386 if isinstance(value, string_types) and py3compat.isidentifier(value):
1387 return value
1388 self.error(obj, value)
1389
1390 class DottedObjectName(ObjectName):
1391 """A string holding a valid dotted object name in Python, such as A.b3._c"""
1392 def validate(self, obj, value):
1393 value = self.coerce_str(obj, value)
1394
1395 if isinstance(value, string_types) and py3compat.isidentifier(value, dotted=True):
1396 return value
1397 self.error(obj, value)
1398
1399
1400 class Bool(TraitType):
1401 """A boolean (True, False) trait."""
1402
1403 default_value = False
1404 info_text = 'a boolean'
1405
1406 def validate(self, obj, value):
1407 if isinstance(value, bool):
1408 return value
1409 self.error(obj, value)
1410
1411
1412 class CBool(Bool):
1413 """A casting version of the boolean trait."""
1414
1415 def validate(self, obj, value):
1416 try:
1417 return bool(value)
1418 except:
1419 self.error(obj, value)
1420
1421
1422 class Enum(TraitType):
1423 """An enum that whose value must be in a given sequence."""
1424
1425 def __init__(self, values, default_value=None, **metadata):
1426 self.values = values
1427 super(Enum, self).__init__(default_value, **metadata)
1428
1429 def validate(self, obj, value):
1430 if value in self.values:
1431 return value
1432 self.error(obj, value)
1433
1434 def info(self):
1435 """ Returns a description of the trait."""
1436 result = 'any of ' + repr(self.values)
1437 if self.allow_none:
1438 return result + ' or None'
1439 return result
1440
1441 class CaselessStrEnum(Enum):
1442 """An enum of strings that are caseless in validate."""
1443
1444 def validate(self, obj, value):
1445 if not isinstance(value, py3compat.string_types):
1446 self.error(obj, value)
1447
1448 for v in self.values:
1449 if v.lower() == value.lower():
1450 return v
1451 self.error(obj, value)
1452
1453 class Container(Instance):
1454 """An instance of a container (list, set, etc.)
1455
1456 To be subclassed by overriding klass.
1457 """
1458 klass = None
1459 _cast_types = ()
1460 _valid_defaults = SequenceTypes
1461 _trait = None
1462
1463 def __init__(self, trait=None, default_value=None, allow_none=False,
1464 **metadata):
1465 """Create a container trait type from a list, set, or tuple.
1466
1467 The default value is created by doing ``List(default_value)``,
1468 which creates a copy of the ``default_value``.
1469
1470 ``trait`` can be specified, which restricts the type of elements
1471 in the container to that TraitType.
1472
1473 If only one arg is given and it is not a Trait, it is taken as
1474 ``default_value``:
1475
1476 ``c = List([1,2,3])``
1477
1478 Parameters
1479 ----------
1480
1481 trait : TraitType [ optional ]
1482 the type for restricting the contents of the Container. If unspecified,
1483 types are not checked.
1484
1485 default_value : SequenceType [ optional ]
1486 The default value for the Trait. Must be list/tuple/set, and
1487 will be cast to the container type.
1488
1489 allow_none : bool [ default False ]
1490 Whether to allow the value to be None
1491
1492 **metadata : any
1493 further keys for extensions to the Trait (e.g. config)
1494
1495 """
1496 # allow List([values]):
1497 if default_value is None and not is_trait(trait):
1498 default_value = trait
1499 trait = None
1500
1501 if default_value is None:
1502 args = ()
1503 elif isinstance(default_value, self._valid_defaults):
1504 args = (default_value,)
1505 else:
1506 raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
1507
1508 if is_trait(trait):
1509 self._trait = trait() if isinstance(trait, type) else trait
1510 self._trait.name = 'element'
1511 elif trait is not None:
1512 raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait))
1513
1514 super(Container,self).__init__(klass=self.klass, args=args,
1515 allow_none=allow_none, **metadata)
1516
1517 def element_error(self, obj, element, validator):
1518 e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \
1519 % (self.name, class_of(obj), validator.info(), repr_type(element))
1520 raise TraitError(e)
1521
1522 def validate(self, obj, value):
1523 if isinstance(value, self._cast_types):
1524 value = self.klass(value)
1525 value = super(Container, self).validate(obj, value)
1526 if value is None:
1527 return value
1528
1529 value = self.validate_elements(obj, value)
1530
1531 return value
1532
1533 def validate_elements(self, obj, value):
1534 validated = []
1535 if self._trait is None or isinstance(self._trait, Any):
1536 return value
1537 for v in value:
1538 try:
1539 v = self._trait._validate(obj, v)
1540 except TraitError:
1541 self.element_error(obj, v, self._trait)
1542 else:
1543 validated.append(v)
1544 return self.klass(validated)
1545
1546 def instance_init(self):
1547 if isinstance(self._trait, TraitType):
1548 self._trait.this_class = self.this_class
1549 self._trait.instance_init()
1550 super(Container, self).instance_init()
1551
1552
1553 class List(Container):
1554 """An instance of a Python list."""
1555 klass = list
1556 _cast_types = (tuple,)
1557
1558 def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, **metadata):
1559 """Create a List trait type from a list, set, or tuple.
1560
1561 The default value is created by doing ``List(default_value)``,
1562 which creates a copy of the ``default_value``.
1563
1564 ``trait`` can be specified, which restricts the type of elements
1565 in the container to that TraitType.
1566
1567 If only one arg is given and it is not a Trait, it is taken as
1568 ``default_value``:
1569
1570 ``c = List([1,2,3])``
1571
1572 Parameters
1573 ----------
1574
1575 trait : TraitType [ optional ]
1576 the type for restricting the contents of the Container. If unspecified,
1577 types are not checked.
1578
1579 default_value : SequenceType [ optional ]
1580 The default value for the Trait. Must be list/tuple/set, and
1581 will be cast to the container type.
1582
1583 minlen : Int [ default 0 ]
1584 The minimum length of the input list
1585
1586 maxlen : Int [ default sys.maxsize ]
1587 The maximum length of the input list
1588
1589 allow_none : bool [ default False ]
1590 Whether to allow the value to be None
1591
1592 **metadata : any
1593 further keys for extensions to the Trait (e.g. config)
1594
1595 """
1596 self._minlen = minlen
1597 self._maxlen = maxlen
1598 super(List, self).__init__(trait=trait, default_value=default_value,
1599 **metadata)
1600
1601 def length_error(self, obj, value):
1602 e = "The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified." \
1603 % (self.name, class_of(obj), self._minlen, self._maxlen, value)
1604 raise TraitError(e)
1605
1606 def validate_elements(self, obj, value):
1607 length = len(value)
1608 if length < self._minlen or length > self._maxlen:
1609 self.length_error(obj, value)
1610
1611 return super(List, self).validate_elements(obj, value)
1612
1613 def validate(self, obj, value):
1614 value = super(List, self).validate(obj, value)
1615 value = self.validate_elements(obj, value)
1616 return value
1617
1618
1619 class Set(List):
1620 """An instance of a Python set."""
1621 klass = set
1622 _cast_types = (tuple, list)
1623
1624
1625 class Tuple(Container):
1626 """An instance of a Python tuple."""
1627 klass = tuple
1628 _cast_types = (list,)
1629
1630 def __init__(self, *traits, **metadata):
1631 """Tuple(*traits, default_value=None, **medatata)
1632
1633 Create a tuple from a list, set, or tuple.
1634
1635 Create a fixed-type tuple with Traits:
1636
1637 ``t = Tuple(Int, Str, CStr)``
1638
1639 would be length 3, with Int,Str,CStr for each element.
1640
1641 If only one arg is given and it is not a Trait, it is taken as
1642 default_value:
1643
1644 ``t = Tuple((1,2,3))``
1645
1646 Otherwise, ``default_value`` *must* be specified by keyword.
1647
1648 Parameters
1649 ----------
1650
1651 *traits : TraitTypes [ optional ]
1652 the types for restricting the contents of the Tuple. If unspecified,
1653 types are not checked. If specified, then each positional argument
1654 corresponds to an element of the tuple. Tuples defined with traits
1655 are of fixed length.
1656
1657 default_value : SequenceType [ optional ]
1658 The default value for the Tuple. Must be list/tuple/set, and
1659 will be cast to a tuple. If `traits` are specified, the
1660 `default_value` must conform to the shape and type they specify.
1661
1662 allow_none : bool [ default False ]
1663 Whether to allow the value to be None
1664
1665 **metadata : any
1666 further keys for extensions to the Trait (e.g. config)
1667
1668 """
1669 default_value = metadata.pop('default_value', None)
1670 allow_none = metadata.pop('allow_none', True)
1671
1672 # allow Tuple((values,)):
1673 if len(traits) == 1 and default_value is None and not is_trait(traits[0]):
1674 default_value = traits[0]
1675 traits = ()
1676
1677 if default_value is None:
1678 args = ()
1679 elif isinstance(default_value, self._valid_defaults):
1680 args = (default_value,)
1681 else:
1682 raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
1683
1684 self._traits = []
1685 for trait in traits:
1686 t = trait() if isinstance(trait, type) else trait
1687 t.name = 'element'
1688 self._traits.append(t)
1689
1690 if self._traits and default_value is None:
1691 # don't allow default to be an empty container if length is specified
1692 args = None
1693 super(Container,self).__init__(klass=self.klass, args=args, allow_none=allow_none, **metadata)
1694
1695 def validate_elements(self, obj, value):
1696 if not self._traits:
1697 # nothing to validate
1698 return value
1699 if len(value) != len(self._traits):
1700 e = "The '%s' trait of %s instance requires %i elements, but a value of %s was specified." \
1701 % (self.name, class_of(obj), len(self._traits), repr_type(value))
1702 raise TraitError(e)
1703
1704 validated = []
1705 for t, v in zip(self._traits, value):
1706 try:
1707 v = t._validate(obj, v)
1708 except TraitError:
1709 self.element_error(obj, v, t)
1710 else:
1711 validated.append(v)
1712 return tuple(validated)
1713
1714 def instance_init(self):
1715 for trait in self._traits:
1716 if isinstance(trait, TraitType):
1717 trait.this_class = self.this_class
1718 trait.instance_init()
1719 super(Container, self).instance_init()
1720
1721
1722 class Dict(Instance):
1723 """An instance of a Python dict."""
1724 _trait = None
1725
1726 def __init__(self, trait=None, default_value=NoDefaultSpecified, allow_none=False, **metadata):
1727 """Create a dict trait type from a dict.
1728
1729 The default value is created by doing ``dict(default_value)``,
1730 which creates a copy of the ``default_value``.
1731
1732 trait : TraitType [ optional ]
1733 the type for restricting the contents of the Container. If unspecified,
1734 types are not checked.
1735
1736 default_value : SequenceType [ optional ]
1737 The default value for the Dict. Must be dict, tuple, or None, and
1738 will be cast to a dict if not None. If `trait` is specified, the
1739 `default_value` must conform to the constraints it specifies.
1740
1741 allow_none : bool [ default False ]
1742 Whether to allow the value to be None
1743
1744 """
1745 if default_value is NoDefaultSpecified and trait is not None:
1746 if not is_trait(trait):
1747 default_value = trait
1748 trait = None
1749 if default_value is NoDefaultSpecified:
1750 default_value = {}
1751 if default_value is None:
1752 args = None
1753 elif isinstance(default_value, dict):
1754 args = (default_value,)
1755 elif isinstance(default_value, SequenceTypes):
1756 args = (default_value,)
1757 else:
1758 raise TypeError('default value of Dict was %s' % default_value)
1759
1760 if is_trait(trait):
1761 self._trait = trait() if isinstance(trait, type) else trait
1762 self._trait.name = 'element'
1763 elif trait is not None:
1764 raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait))
1765
1766 super(Dict,self).__init__(klass=dict, args=args,
1767 allow_none=allow_none, **metadata)
1768
1769 def element_error(self, obj, element, validator):
1770 e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \
1771 % (self.name, class_of(obj), validator.info(), repr_type(element))
1772 raise TraitError(e)
1773
1774 def validate(self, obj, value):
1775 value = super(Dict, self).validate(obj, value)
1776 if value is None:
1777 return value
1778 value = self.validate_elements(obj, value)
1779 return value
1780
1781 def validate_elements(self, obj, value):
1782 if self._trait is None or isinstance(self._trait, Any):
1783 return value
1784 validated = {}
1785 for key in value:
1786 v = value[key]
1787 try:
1788 v = self._trait._validate(obj, v)
1789 except TraitError:
1790 self.element_error(obj, v, self._trait)
1791 else:
1792 validated[key] = v
1793 return self.klass(validated)
1794
1795 def instance_init(self):
1796 if isinstance(self._trait, TraitType):
1797 self._trait.this_class = self.this_class
1798 self._trait.instance_init()
1799 super(Dict, self).instance_init()
1800
1801
1802 class EventfulDict(Instance):
1803 """An instance of an EventfulDict."""
1804
1805 def __init__(self, default_value={}, allow_none=False, **metadata):
1806 """Create a EventfulDict trait type from a dict.
1807
1808 The default value is created by doing
1809 ``eventful.EvenfulDict(default_value)``, which creates a copy of the
1810 ``default_value``.
1811 """
1812 if default_value is None:
1813 args = None
1814 elif isinstance(default_value, dict):
1815 args = (default_value,)
1816 elif isinstance(default_value, SequenceTypes):
1817 args = (default_value,)
1818 else:
1819 raise TypeError('default value of EventfulDict was %s' % default_value)
1820
1821 super(EventfulDict, self).__init__(klass=eventful.EventfulDict, args=args,
1822 allow_none=allow_none, **metadata)
1823
1824
1825 class EventfulList(Instance):
1826 """An instance of an EventfulList."""
1827
1828 def __init__(self, default_value=None, allow_none=False, **metadata):
1829 """Create a EventfulList trait type from a dict.
1830
1831 The default value is created by doing
1832 ``eventful.EvenfulList(default_value)``, which creates a copy of the
1833 ``default_value``.
1834 """
1835 if default_value is None:
1836 args = ((),)
1837 else:
1838 args = (default_value,)
1839
1840 super(EventfulList, self).__init__(klass=eventful.EventfulList, args=args,
1841 allow_none=allow_none, **metadata)
1842
1843
1844 class TCPAddress(TraitType):
1845 """A trait for an (ip, port) tuple.
1846
1847 This allows for both IPv4 IP addresses as well as hostnames.
1848 """
1849
1850 default_value = ('127.0.0.1', 0)
1851 info_text = 'an (ip, port) tuple'
1852
1853 def validate(self, obj, value):
1854 if isinstance(value, tuple):
1855 if len(value) == 2:
1856 if isinstance(value[0], py3compat.string_types) and isinstance(value[1], int):
1857 port = value[1]
1858 if port >= 0 and port <= 65535:
1859 return value
1860 self.error(obj, value)
1861
1862 class CRegExp(TraitType):
1863 """A casting compiled regular expression trait.
1864
1865 Accepts both strings and compiled regular expressions. The resulting
1866 attribute will be a compiled regular expression."""
1867
1868 info_text = 'a regular expression'
1869
1870 def validate(self, obj, value):
1871 try:
1872 return re.compile(value)
1873 except:
1874 self.error(obj, value)
@@ -1,20 +1,20 b''
1 1 """Test trait types of the widget packages."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from unittest import TestCase
7 7 from IPython.utils.traitlets import HasTraits
8 from IPython.utils.tests.test_traitlets import TraitTestBase
8 from traitlets.tests.test_traitlets import TraitTestBase
9 9 from IPython.html.widgets import Color
10 10
11 11
12 12 class ColorTrait(HasTraits):
13 13 value = Color("black")
14 14
15 15
16 16 class TestColor(TraitTestBase):
17 17 obj = ColorTrait()
18 18
19 19 _good_values = ["blue", "#AA0", "#FFFFFF"]
20 20 _bad_values = ["vanilla", "blues"]
@@ -1,520 +1,517 b''
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Suite Runner.
3 3
4 4 This module provides a main entry point to a user script to test IPython
5 5 itself from the command line. There are two ways of running this script:
6 6
7 7 1. With the syntax `iptest all`. This runs our entire test suite by
8 8 calling this script (with different arguments) recursively. This
9 9 causes modules and package to be tested in different processes, using nose
10 10 or trial where appropriate.
11 11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 12 the script simply calls nose, but with special command line flags and
13 13 plugins loaded.
14 14
15 15 """
16 16
17 17 # Copyright (c) IPython Development Team.
18 18 # Distributed under the terms of the Modified BSD License.
19 19
20 20 from __future__ import print_function
21 21
22 22 import glob
23 23 from io import BytesIO
24 24 import os
25 25 import os.path as path
26 26 import sys
27 27 from threading import Thread, Lock, Event
28 28 import warnings
29 29
30 30 import nose.plugins.builtin
31 31 from nose.plugins.xunit import Xunit
32 32 from nose import SkipTest
33 33 from nose.core import TestProgram
34 34 from nose.plugins import Plugin
35 35 from nose.util import safe_str
36 36
37 37 from IPython.utils.process import is_cmd_found
38 38 from IPython.utils.py3compat import bytes_to_str
39 39 from IPython.utils.importstring import import_item
40 40 from IPython.testing.plugin.ipdoctest import IPythonDoctest
41 41 from IPython.external.decorators import KnownFailure, knownfailureif
42 42
43 43 pjoin = path.join
44 44
45 45 #-----------------------------------------------------------------------------
46 46 # Warnings control
47 47 #-----------------------------------------------------------------------------
48 48
49 49 # Twisted generates annoying warnings with Python 2.6, as will do other code
50 50 # that imports 'sets' as of today
51 51 warnings.filterwarnings('ignore', 'the sets module is deprecated',
52 52 DeprecationWarning )
53 53
54 54 # This one also comes from Twisted
55 55 warnings.filterwarnings('ignore', 'the sha module is deprecated',
56 56 DeprecationWarning)
57 57
58 58 # Wx on Fedora11 spits these out
59 59 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
60 60 UserWarning)
61 61
62 62 # ------------------------------------------------------------------------------
63 63 # Monkeypatch Xunit to count known failures as skipped.
64 64 # ------------------------------------------------------------------------------
65 65 def monkeypatch_xunit():
66 66 try:
67 67 knownfailureif(True)(lambda: None)()
68 68 except Exception as e:
69 69 KnownFailureTest = type(e)
70 70
71 71 def addError(self, test, err, capt=None):
72 72 if issubclass(err[0], KnownFailureTest):
73 73 err = (SkipTest,) + err[1:]
74 74 return self.orig_addError(test, err, capt)
75 75
76 76 Xunit.orig_addError = Xunit.addError
77 77 Xunit.addError = addError
78 78
79 79 #-----------------------------------------------------------------------------
80 80 # Check which dependencies are installed and greater than minimum version.
81 81 #-----------------------------------------------------------------------------
82 82 def extract_version(mod):
83 83 return mod.__version__
84 84
85 85 def test_for(item, min_version=None, callback=extract_version):
86 86 """Test to see if item is importable, and optionally check against a minimum
87 87 version.
88 88
89 89 If min_version is given, the default behavior is to check against the
90 90 `__version__` attribute of the item, but specifying `callback` allows you to
91 91 extract the value you are interested in. e.g::
92 92
93 93 In [1]: import sys
94 94
95 95 In [2]: from IPython.testing.iptest import test_for
96 96
97 97 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
98 98 Out[3]: True
99 99
100 100 """
101 101 try:
102 102 check = import_item(item)
103 103 except (ImportError, RuntimeError):
104 104 # GTK reports Runtime error if it can't be initialized even if it's
105 105 # importable.
106 106 return False
107 107 else:
108 108 if min_version:
109 109 if callback:
110 110 # extra processing step to get version to compare
111 111 check = callback(check)
112 112
113 113 return check >= min_version
114 114 else:
115 115 return True
116 116
117 117 # Global dict where we can store information on what we have and what we don't
118 118 # have available at test run time
119 119 have = {}
120 120
121 121 have['curses'] = test_for('_curses')
122 122 have['matplotlib'] = test_for('matplotlib')
123 123 have['numpy'] = test_for('numpy')
124 124 have['pexpect'] = test_for('pexpect')
125 125 have['pymongo'] = test_for('pymongo')
126 126 have['pygments'] = test_for('pygments')
127 127 have['qt'] = test_for('IPython.external.qt')
128 128 have['sqlite3'] = test_for('sqlite3')
129 129 have['tornado'] = test_for('tornado.version_info', (4,0), callback=None)
130 130 have['jinja2'] = test_for('jinja2')
131 131 have['mistune'] = test_for('mistune')
132 132 have['requests'] = test_for('requests')
133 133 have['sphinx'] = test_for('sphinx')
134 134 have['jsonschema'] = test_for('jsonschema')
135 135 have['terminado'] = test_for('terminado')
136 136 have['casperjs'] = is_cmd_found('casperjs')
137 137 have['phantomjs'] = is_cmd_found('phantomjs')
138 138 have['slimerjs'] = is_cmd_found('slimerjs')
139 139
140 140 min_zmq = (13,)
141 141
142 142 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
143 143
144 144 #-----------------------------------------------------------------------------
145 145 # Test suite definitions
146 146 #-----------------------------------------------------------------------------
147 147
148 148 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
149 149 'extensions', 'lib', 'terminal', 'testing', 'utils',
150 150 'nbformat', 'qt', 'html', 'nbconvert'
151 151 ]
152 152
153 153 class TestSection(object):
154 154 def __init__(self, name, includes):
155 155 self.name = name
156 156 self.includes = includes
157 157 self.excludes = []
158 158 self.dependencies = []
159 159 self.enabled = True
160 160
161 161 def exclude(self, module):
162 162 if not module.startswith('IPython'):
163 163 module = self.includes[0] + "." + module
164 164 self.excludes.append(module.replace('.', os.sep))
165 165
166 166 def requires(self, *packages):
167 167 self.dependencies.extend(packages)
168 168
169 169 @property
170 170 def will_run(self):
171 171 return self.enabled and all(have[p] for p in self.dependencies)
172 172
173 173 shims = {
174 174 'parallel': 'ipython_parallel',
175 175 'kernel': 'ipython_kernel',
176 176 'kernel.inprocess': 'ipython_kernel.inprocess',
177 'config': 'traitlets',
177 178 }
178 179
179 180 # Name -> (include, exclude, dependencies_met)
180 181 test_sections = {n:TestSection(n, [shims.get(n, 'IPython.%s' % n)]) for n in test_group_names}
181 182
182 183
183 184 # Exclusions and dependencies
184 185 # ---------------------------
185 186
186 187 # core:
187 188 sec = test_sections['core']
188 189 if not have['sqlite3']:
189 190 sec.exclude('tests.test_history')
190 191 sec.exclude('history')
191 192 if not have['matplotlib']:
192 193 sec.exclude('pylabtools'),
193 194 sec.exclude('tests.test_pylabtools')
194 195
195 196 # lib:
196 197 sec = test_sections['lib']
197 198 if not have['zmq']:
198 199 sec.exclude('kernel')
199 200 # We do this unconditionally, so that the test suite doesn't import
200 201 # gtk, changing the default encoding and masking some unicode bugs.
201 202 sec.exclude('inputhookgtk')
202 203 # We also do this unconditionally, because wx can interfere with Unix signals.
203 204 # There are currently no tests for it anyway.
204 205 sec.exclude('inputhookwx')
205 206 # Testing inputhook will need a lot of thought, to figure out
206 207 # how to have tests that don't lock up with the gui event
207 208 # loops in the picture
208 209 sec.exclude('inputhook')
209 210
210 211 # testing:
211 212 sec = test_sections['testing']
212 213 # These have to be skipped on win32 because they use echo, rm, cd, etc.
213 214 # See ticket https://github.com/ipython/ipython/issues/87
214 215 if sys.platform == 'win32':
215 216 sec.exclude('plugin.test_exampleip')
216 217 sec.exclude('plugin.dtexample')
217 218
218 219 # terminal:
219 220 if (not have['pexpect']) or (not have['zmq']):
220 221 test_sections['terminal'].exclude('console')
221 222
222 223 # parallel
223 224 sec = test_sections['parallel']
224 225 sec.requires('zmq')
225 226 if not have['pymongo']:
226 227 sec.exclude('controller.mongodb')
227 228 sec.exclude('tests.test_mongodb')
228 229
229 230 # kernel:
230 231 sec = test_sections['kernel']
231 232 sec.requires('zmq')
232 233 # The in-process kernel tests are done in a separate section
233 234 sec.exclude('inprocess')
234 235 # importing gtk sets the default encoding, which we want to avoid
235 236 sec.exclude('gui.gtkembed')
236 237 sec.exclude('gui.gtk3embed')
237 238 if not have['matplotlib']:
238 239 sec.exclude('pylab')
239 240
240 241 # kernel.inprocess:
241 242 test_sections['kernel.inprocess'].requires('zmq')
242 243
243 244 # extensions:
244 245 sec = test_sections['extensions']
245 246 # This is deprecated in favour of rpy2
246 247 sec.exclude('rmagic')
247 248 # autoreload does some strange stuff, so move it to its own test section
248 249 sec.exclude('autoreload')
249 250 sec.exclude('tests.test_autoreload')
250 251 test_sections['autoreload'] = TestSection('autoreload',
251 252 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
252 253 test_group_names.append('autoreload')
253 254
254 255 # qt:
255 256 test_sections['qt'].requires('zmq', 'qt', 'pygments')
256 257
257 258 # html:
258 259 sec = test_sections['html']
259 260 sec.requires('zmq', 'tornado', 'requests', 'sqlite3', 'jsonschema')
260 261 # The notebook 'static' directory contains JS, css and other
261 262 # files for web serving. Occasionally projects may put a .py
262 263 # file in there (MathJax ships a conf.py), so we might as
263 264 # well play it safe and skip the whole thing.
264 265 sec.exclude('static')
265 266 sec.exclude('tasks')
266 267 if not have['jinja2']:
267 268 sec.exclude('notebookapp')
268 269 if not have['pygments'] or not have['jinja2']:
269 270 sec.exclude('nbconvert')
270 271 if not have['terminado']:
271 272 sec.exclude('terminal')
272 273
273 # config:
274 # Config files aren't really importable stand-alone
275 test_sections['config'].exclude('profile')
276
277 274 # nbconvert:
278 275 sec = test_sections['nbconvert']
279 276 sec.requires('pygments', 'jinja2', 'jsonschema', 'mistune')
280 277 # Exclude nbconvert directories containing config files used to test.
281 278 # Executing the config files with iptest would cause an exception.
282 279 sec.exclude('tests.files')
283 280 sec.exclude('exporters.tests.files')
284 281 if not have['tornado']:
285 282 sec.exclude('nbconvert.post_processors.serve')
286 283 sec.exclude('nbconvert.post_processors.tests.test_serve')
287 284
288 285 # nbformat:
289 286 test_sections['nbformat'].requires('jsonschema')
290 287
291 288 #-----------------------------------------------------------------------------
292 289 # Functions and classes
293 290 #-----------------------------------------------------------------------------
294 291
295 292 def check_exclusions_exist():
296 293 from IPython.utils.path import get_ipython_package_dir
297 294 from IPython.utils.warn import warn
298 295 parent = os.path.dirname(get_ipython_package_dir())
299 296 for sec in test_sections:
300 297 for pattern in sec.exclusions:
301 298 fullpath = pjoin(parent, pattern)
302 299 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
303 300 warn("Excluding nonexistent file: %r" % pattern)
304 301
305 302
306 303 class ExclusionPlugin(Plugin):
307 304 """A nose plugin to effect our exclusions of files and directories.
308 305 """
309 306 name = 'exclusions'
310 307 score = 3000 # Should come before any other plugins
311 308
312 309 def __init__(self, exclude_patterns=None):
313 310 """
314 311 Parameters
315 312 ----------
316 313
317 314 exclude_patterns : sequence of strings, optional
318 315 Filenames containing these patterns (as raw strings, not as regular
319 316 expressions) are excluded from the tests.
320 317 """
321 318 self.exclude_patterns = exclude_patterns or []
322 319 super(ExclusionPlugin, self).__init__()
323 320
324 321 def options(self, parser, env=os.environ):
325 322 Plugin.options(self, parser, env)
326 323
327 324 def configure(self, options, config):
328 325 Plugin.configure(self, options, config)
329 326 # Override nose trying to disable plugin.
330 327 self.enabled = True
331 328
332 329 def wantFile(self, filename):
333 330 """Return whether the given filename should be scanned for tests.
334 331 """
335 332 if any(pat in filename for pat in self.exclude_patterns):
336 333 return False
337 334 return None
338 335
339 336 def wantDirectory(self, directory):
340 337 """Return whether the given directory should be scanned for tests.
341 338 """
342 339 if any(pat in directory for pat in self.exclude_patterns):
343 340 return False
344 341 return None
345 342
346 343
347 344 class StreamCapturer(Thread):
348 345 daemon = True # Don't hang if main thread crashes
349 346 started = False
350 347 def __init__(self, echo=False):
351 348 super(StreamCapturer, self).__init__()
352 349 self.echo = echo
353 350 self.streams = []
354 351 self.buffer = BytesIO()
355 352 self.readfd, self.writefd = os.pipe()
356 353 self.buffer_lock = Lock()
357 354 self.stop = Event()
358 355
359 356 def run(self):
360 357 self.started = True
361 358
362 359 while not self.stop.is_set():
363 360 chunk = os.read(self.readfd, 1024)
364 361
365 362 with self.buffer_lock:
366 363 self.buffer.write(chunk)
367 364 if self.echo:
368 365 sys.stdout.write(bytes_to_str(chunk))
369 366
370 367 os.close(self.readfd)
371 368 os.close(self.writefd)
372 369
373 370 def reset_buffer(self):
374 371 with self.buffer_lock:
375 372 self.buffer.truncate(0)
376 373 self.buffer.seek(0)
377 374
378 375 def get_buffer(self):
379 376 with self.buffer_lock:
380 377 return self.buffer.getvalue()
381 378
382 379 def ensure_started(self):
383 380 if not self.started:
384 381 self.start()
385 382
386 383 def halt(self):
387 384 """Safely stop the thread."""
388 385 if not self.started:
389 386 return
390 387
391 388 self.stop.set()
392 389 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
393 390 self.join()
394 391
395 392 class SubprocessStreamCapturePlugin(Plugin):
396 393 name='subprocstreams'
397 394 def __init__(self):
398 395 Plugin.__init__(self)
399 396 self.stream_capturer = StreamCapturer()
400 397 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
401 398 # This is ugly, but distant parts of the test machinery need to be able
402 399 # to redirect streams, so we make the object globally accessible.
403 400 nose.iptest_stdstreams_fileno = self.get_write_fileno
404 401
405 402 def get_write_fileno(self):
406 403 if self.destination == 'capture':
407 404 self.stream_capturer.ensure_started()
408 405 return self.stream_capturer.writefd
409 406 elif self.destination == 'discard':
410 407 return os.open(os.devnull, os.O_WRONLY)
411 408 else:
412 409 return sys.__stdout__.fileno()
413 410
414 411 def configure(self, options, config):
415 412 Plugin.configure(self, options, config)
416 413 # Override nose trying to disable plugin.
417 414 if self.destination == 'capture':
418 415 self.enabled = True
419 416
420 417 def startTest(self, test):
421 418 # Reset log capture
422 419 self.stream_capturer.reset_buffer()
423 420
424 421 def formatFailure(self, test, err):
425 422 # Show output
426 423 ec, ev, tb = err
427 424 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
428 425 if captured.strip():
429 426 ev = safe_str(ev)
430 427 out = [ev, '>> begin captured subprocess output <<',
431 428 captured,
432 429 '>> end captured subprocess output <<']
433 430 return ec, '\n'.join(out), tb
434 431
435 432 return err
436 433
437 434 formatError = formatFailure
438 435
439 436 def finalize(self, result):
440 437 self.stream_capturer.halt()
441 438
442 439
443 440 def run_iptest():
444 441 """Run the IPython test suite using nose.
445 442
446 443 This function is called when this script is **not** called with the form
447 444 `iptest all`. It simply calls nose with appropriate command line flags
448 445 and accepts all of the standard nose arguments.
449 446 """
450 447 # Apply our monkeypatch to Xunit
451 448 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
452 449 monkeypatch_xunit()
453 450
454 451 warnings.filterwarnings('ignore',
455 452 'This will be removed soon. Use IPython.testing.util instead')
456 453
457 454 arg1 = sys.argv[1]
458 455 if arg1 in test_sections:
459 456 section = test_sections[arg1]
460 457 sys.argv[1:2] = section.includes
461 458 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
462 459 section = test_sections[arg1[8:]]
463 460 sys.argv[1:2] = section.includes
464 461 else:
465 462 section = TestSection(arg1, includes=[arg1])
466 463
467 464
468 465 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
469 466
470 467 '--with-ipdoctest',
471 468 '--ipdoctest-tests','--ipdoctest-extension=txt',
472 469
473 470 # We add --exe because of setuptools' imbecility (it
474 471 # blindly does chmod +x on ALL files). Nose does the
475 472 # right thing and it tries to avoid executables,
476 473 # setuptools unfortunately forces our hand here. This
477 474 # has been discussed on the distutils list and the
478 475 # setuptools devs refuse to fix this problem!
479 476 '--exe',
480 477 ]
481 478 if '-a' not in argv and '-A' not in argv:
482 479 argv = argv + ['-a', '!crash']
483 480
484 481 if nose.__version__ >= '0.11':
485 482 # I don't fully understand why we need this one, but depending on what
486 483 # directory the test suite is run from, if we don't give it, 0 tests
487 484 # get run. Specifically, if the test suite is run from the source dir
488 485 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
489 486 # even if the same call done in this directory works fine). It appears
490 487 # that if the requested package is in the current dir, nose bails early
491 488 # by default. Since it's otherwise harmless, leave it in by default
492 489 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
493 490 argv.append('--traverse-namespace')
494 491
495 492 # use our plugin for doctesting. It will remove the standard doctest plugin
496 493 # if it finds it enabled
497 494 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
498 495 SubprocessStreamCapturePlugin() ]
499 496
500 497 # Use working directory set by parent process (see iptestcontroller)
501 498 if 'IPTEST_WORKING_DIR' in os.environ:
502 499 os.chdir(os.environ['IPTEST_WORKING_DIR'])
503 500
504 501 # We need a global ipython running in this process, but the special
505 502 # in-process group spawns its own IPython kernels, so for *that* group we
506 503 # must avoid also opening the global one (otherwise there's a conflict of
507 504 # singletons). Ultimately the solution to this problem is to refactor our
508 505 # assumptions about what needs to be a singleton and what doesn't (app
509 506 # objects should, individual shells shouldn't). But for now, this
510 507 # workaround allows the test suite for the inprocess module to complete.
511 508 if 'kernel.inprocess' not in section.name:
512 509 from IPython.testing import globalipapp
513 510 globalipapp.start_ipython()
514 511
515 512 # Now nose can run
516 513 TestProgram(argv=argv, addplugins=plugins)
517 514
518 515 if __name__ == '__main__':
519 516 run_iptest()
520 517
@@ -1,495 +1,498 b''
1 1 """Generic testing tools.
2 2
3 3 Authors
4 4 -------
5 5 - Fernando Perez <Fernando.Perez@berkeley.edu>
6 6 """
7 7
8 8 from __future__ import absolute_import
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Copyright (C) 2009 The IPython Development Team
12 12 #
13 13 # Distributed under the terms of the BSD License. The full license is in
14 14 # the file COPYING, distributed as part of this software.
15 15 #-----------------------------------------------------------------------------
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 import os
22 22 import re
23 23 import sys
24 24 import tempfile
25 25
26 26 from contextlib import contextmanager
27 27 from io import StringIO
28 28 from subprocess import Popen, PIPE
29 29
30 30 try:
31 31 # These tools are used by parts of the runtime, so we make the nose
32 32 # dependency optional at this point. Nose is a hard dependency to run the
33 33 # test suite, but NOT to use ipython itself.
34 34 import nose.tools as nt
35 35 has_nose = True
36 36 except ImportError:
37 37 has_nose = False
38 38
39 39 from IPython.config.loader import Config
40 40 from IPython.utils.process import get_output_error_code
41 41 from IPython.utils.text import list_strings
42 42 from IPython.utils.io import temp_pyfile, Tee
43 43 from IPython.utils import py3compat
44 44 from IPython.utils.encoding import DEFAULT_ENCODING
45 45
46 46 from . import decorators as dec
47 47 from . import skipdoctest
48 48
49 49 #-----------------------------------------------------------------------------
50 50 # Functions and classes
51 51 #-----------------------------------------------------------------------------
52 52
53 53 # The docstring for full_path doctests differently on win32 (different path
54 54 # separator) so just skip the doctest there. The example remains informative.
55 55 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
56 56
57 57 @doctest_deco
58 58 def full_path(startPath,files):
59 59 """Make full paths for all the listed files, based on startPath.
60 60
61 61 Only the base part of startPath is kept, since this routine is typically
62 62 used with a script's ``__file__`` variable as startPath. The base of startPath
63 63 is then prepended to all the listed files, forming the output list.
64 64
65 65 Parameters
66 66 ----------
67 67 startPath : string
68 68 Initial path to use as the base for the results. This path is split
69 69 using os.path.split() and only its first component is kept.
70 70
71 71 files : string or list
72 72 One or more files.
73 73
74 74 Examples
75 75 --------
76 76
77 77 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
78 78 ['/foo/a.txt', '/foo/b.txt']
79 79
80 80 >>> full_path('/foo',['a.txt','b.txt'])
81 81 ['/a.txt', '/b.txt']
82 82
83 83 If a single file is given, the output is still a list::
84 84
85 85 >>> full_path('/foo','a.txt')
86 86 ['/a.txt']
87 87 """
88 88
89 89 files = list_strings(files)
90 90 base = os.path.split(startPath)[0]
91 91 return [ os.path.join(base,f) for f in files ]
92 92
93 93
94 94 def parse_test_output(txt):
95 95 """Parse the output of a test run and return errors, failures.
96 96
97 97 Parameters
98 98 ----------
99 99 txt : str
100 100 Text output of a test run, assumed to contain a line of one of the
101 101 following forms::
102 102
103 103 'FAILED (errors=1)'
104 104 'FAILED (failures=1)'
105 105 'FAILED (errors=1, failures=1)'
106 106
107 107 Returns
108 108 -------
109 109 nerr, nfail
110 110 number of errors and failures.
111 111 """
112 112
113 113 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
114 114 if err_m:
115 115 nerr = int(err_m.group(1))
116 116 nfail = 0
117 117 return nerr, nfail
118 118
119 119 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
120 120 if fail_m:
121 121 nerr = 0
122 122 nfail = int(fail_m.group(1))
123 123 return nerr, nfail
124 124
125 125 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
126 126 re.MULTILINE)
127 127 if both_m:
128 128 nerr = int(both_m.group(1))
129 129 nfail = int(both_m.group(2))
130 130 return nerr, nfail
131 131
132 132 # If the input didn't match any of these forms, assume no error/failures
133 133 return 0, 0
134 134
135 135
136 136 # So nose doesn't think this is a test
137 137 parse_test_output.__test__ = False
138 138
139 139
140 140 def default_argv():
141 141 """Return a valid default argv for creating testing instances of ipython"""
142 142
143 143 return ['--quick', # so no config file is loaded
144 144 # Other defaults to minimize side effects on stdout
145 145 '--colors=NoColor', '--no-term-title','--no-banner',
146 146 '--autocall=0']
147 147
148 148
149 149 def default_config():
150 150 """Return a config object with good defaults for testing."""
151 151 config = Config()
152 152 config.TerminalInteractiveShell.colors = 'NoColor'
153 153 config.TerminalTerminalInteractiveShell.term_title = False,
154 154 config.TerminalInteractiveShell.autocall = 0
155 155 f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
156 156 config.HistoryManager.hist_file = f.name
157 157 f.close()
158 158 config.HistoryManager.db_cache_size = 10000
159 159 return config
160 160
161 161
162 162 def get_ipython_cmd(as_string=False):
163 163 """
164 164 Return appropriate IPython command line name. By default, this will return
165 165 a list that can be used with subprocess.Popen, for example, but passing
166 166 `as_string=True` allows for returning the IPython command as a string.
167 167
168 168 Parameters
169 169 ----------
170 170 as_string: bool
171 171 Flag to allow to return the command as a string.
172 172 """
173 173 ipython_cmd = [sys.executable, "-m", "IPython"]
174 174
175 175 if as_string:
176 176 ipython_cmd = " ".join(ipython_cmd)
177 177
178 178 return ipython_cmd
179 179
180 180 def ipexec(fname, options=None, commands=()):
181 181 """Utility to call 'ipython filename'.
182 182
183 183 Starts IPython with a minimal and safe configuration to make startup as fast
184 184 as possible.
185 185
186 186 Note that this starts IPython in a subprocess!
187 187
188 188 Parameters
189 189 ----------
190 190 fname : str
191 191 Name of file to be executed (should have .py or .ipy extension).
192 192
193 193 options : optional, list
194 194 Extra command-line flags to be passed to IPython.
195 195
196 196 commands : optional, list
197 197 Commands to send in on stdin
198 198
199 199 Returns
200 200 -------
201 201 (stdout, stderr) of ipython subprocess.
202 202 """
203 203 if options is None: options = []
204 204
205 205 # For these subprocess calls, eliminate all prompt printing so we only see
206 206 # output from script execution
207 207 prompt_opts = [ '--PromptManager.in_template=""',
208 208 '--PromptManager.in2_template=""',
209 209 '--PromptManager.out_template=""'
210 210 ]
211 211 cmdargs = default_argv() + prompt_opts + options
212 212
213 213 test_dir = os.path.dirname(__file__)
214 214
215 215 ipython_cmd = get_ipython_cmd()
216 216 # Absolute path for filename
217 217 full_fname = os.path.join(test_dir, fname)
218 218 full_cmd = ipython_cmd + cmdargs + [full_fname]
219 219 env = os.environ.copy()
220 env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
220 # FIXME: ignore all warnings in ipexec while we have shims
221 # should we keep suppressing warnings here, even after removing shims?
222 env['PYTHONWARNINGS'] = 'ignore'
223 # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
221 224 for k, v in env.items():
222 225 # Debug a bizarre failure we've seen on Windows:
223 226 # TypeError: environment can only contain strings
224 227 if not isinstance(v, str):
225 228 print(k, v)
226 229 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
227 230 out, err = p.communicate(input=py3compat.str_to_bytes('\n'.join(commands)) or None)
228 231 out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err)
229 232 # `import readline` causes 'ESC[?1034h' to be output sometimes,
230 233 # so strip that out before doing comparisons
231 234 if out:
232 235 out = re.sub(r'\x1b\[[^h]+h', '', out)
233 236 return out, err
234 237
235 238
236 239 def ipexec_validate(fname, expected_out, expected_err='',
237 240 options=None, commands=()):
238 241 """Utility to call 'ipython filename' and validate output/error.
239 242
240 243 This function raises an AssertionError if the validation fails.
241 244
242 245 Note that this starts IPython in a subprocess!
243 246
244 247 Parameters
245 248 ----------
246 249 fname : str
247 250 Name of the file to be executed (should have .py or .ipy extension).
248 251
249 252 expected_out : str
250 253 Expected stdout of the process.
251 254
252 255 expected_err : optional, str
253 256 Expected stderr of the process.
254 257
255 258 options : optional, list
256 259 Extra command-line flags to be passed to IPython.
257 260
258 261 Returns
259 262 -------
260 263 None
261 264 """
262 265
263 266 import nose.tools as nt
264 267
265 268 out, err = ipexec(fname, options, commands)
266 269 #print 'OUT', out # dbg
267 270 #print 'ERR', err # dbg
268 271 # If there are any errors, we must check those befor stdout, as they may be
269 272 # more informative than simply having an empty stdout.
270 273 if err:
271 274 if expected_err:
272 275 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
273 276 else:
274 277 raise ValueError('Running file %r produced error: %r' %
275 278 (fname, err))
276 279 # If no errors or output on stderr was expected, match stdout
277 280 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
278 281
279 282
280 283 class TempFileMixin(object):
281 284 """Utility class to create temporary Python/IPython files.
282 285
283 286 Meant as a mixin class for test cases."""
284 287
285 288 def mktmp(self, src, ext='.py'):
286 289 """Make a valid python temp file."""
287 290 fname, f = temp_pyfile(src, ext)
288 291 self.tmpfile = f
289 292 self.fname = fname
290 293
291 294 def tearDown(self):
292 295 if hasattr(self, 'tmpfile'):
293 296 # If the tmpfile wasn't made because of skipped tests, like in
294 297 # win32, there's nothing to cleanup.
295 298 self.tmpfile.close()
296 299 try:
297 300 os.unlink(self.fname)
298 301 except:
299 302 # On Windows, even though we close the file, we still can't
300 303 # delete it. I have no clue why
301 304 pass
302 305
303 306 pair_fail_msg = ("Testing {0}\n\n"
304 307 "In:\n"
305 308 " {1!r}\n"
306 309 "Expected:\n"
307 310 " {2!r}\n"
308 311 "Got:\n"
309 312 " {3!r}\n")
310 313 def check_pairs(func, pairs):
311 314 """Utility function for the common case of checking a function with a
312 315 sequence of input/output pairs.
313 316
314 317 Parameters
315 318 ----------
316 319 func : callable
317 320 The function to be tested. Should accept a single argument.
318 321 pairs : iterable
319 322 A list of (input, expected_output) tuples.
320 323
321 324 Returns
322 325 -------
323 326 None. Raises an AssertionError if any output does not match the expected
324 327 value.
325 328 """
326 329 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
327 330 for inp, expected in pairs:
328 331 out = func(inp)
329 332 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
330 333
331 334
332 335 if py3compat.PY3:
333 336 MyStringIO = StringIO
334 337 else:
335 338 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
336 339 # so we need a class that can handle both.
337 340 class MyStringIO(StringIO):
338 341 def write(self, s):
339 342 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
340 343 super(MyStringIO, self).write(s)
341 344
342 345 _re_type = type(re.compile(r''))
343 346
344 347 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
345 348 -------
346 349 {2!s}
347 350 -------
348 351 """
349 352
350 353 class AssertPrints(object):
351 354 """Context manager for testing that code prints certain text.
352 355
353 356 Examples
354 357 --------
355 358 >>> with AssertPrints("abc", suppress=False):
356 359 ... print("abcd")
357 360 ... print("def")
358 361 ...
359 362 abcd
360 363 def
361 364 """
362 365 def __init__(self, s, channel='stdout', suppress=True):
363 366 self.s = s
364 367 if isinstance(self.s, (py3compat.string_types, _re_type)):
365 368 self.s = [self.s]
366 369 self.channel = channel
367 370 self.suppress = suppress
368 371
369 372 def __enter__(self):
370 373 self.orig_stream = getattr(sys, self.channel)
371 374 self.buffer = MyStringIO()
372 375 self.tee = Tee(self.buffer, channel=self.channel)
373 376 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
374 377
375 378 def __exit__(self, etype, value, traceback):
376 379 try:
377 380 if value is not None:
378 381 # If an error was raised, don't check anything else
379 382 return False
380 383 self.tee.flush()
381 384 setattr(sys, self.channel, self.orig_stream)
382 385 printed = self.buffer.getvalue()
383 386 for s in self.s:
384 387 if isinstance(s, _re_type):
385 388 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
386 389 else:
387 390 assert s in printed, notprinted_msg.format(s, self.channel, printed)
388 391 return False
389 392 finally:
390 393 self.tee.close()
391 394
392 395 printed_msg = """Found {0!r} in printed output (on {1}):
393 396 -------
394 397 {2!s}
395 398 -------
396 399 """
397 400
398 401 class AssertNotPrints(AssertPrints):
399 402 """Context manager for checking that certain output *isn't* produced.
400 403
401 404 Counterpart of AssertPrints"""
402 405 def __exit__(self, etype, value, traceback):
403 406 try:
404 407 if value is not None:
405 408 # If an error was raised, don't check anything else
406 409 self.tee.close()
407 410 return False
408 411 self.tee.flush()
409 412 setattr(sys, self.channel, self.orig_stream)
410 413 printed = self.buffer.getvalue()
411 414 for s in self.s:
412 415 if isinstance(s, _re_type):
413 416 assert not s.search(printed),printed_msg.format(
414 417 s.pattern, self.channel, printed)
415 418 else:
416 419 assert s not in printed, printed_msg.format(
417 420 s, self.channel, printed)
418 421 return False
419 422 finally:
420 423 self.tee.close()
421 424
422 425 @contextmanager
423 426 def mute_warn():
424 427 from IPython.utils import warn
425 428 save_warn = warn.warn
426 429 warn.warn = lambda *a, **kw: None
427 430 try:
428 431 yield
429 432 finally:
430 433 warn.warn = save_warn
431 434
432 435 @contextmanager
433 436 def make_tempfile(name):
434 437 """ Create an empty, named, temporary file for the duration of the context.
435 438 """
436 439 f = open(name, 'w')
437 440 f.close()
438 441 try:
439 442 yield
440 443 finally:
441 444 os.unlink(name)
442 445
443 446
444 447 @contextmanager
445 448 def monkeypatch(obj, name, attr):
446 449 """
447 450 Context manager to replace attribute named `name` in `obj` with `attr`.
448 451 """
449 452 orig = getattr(obj, name)
450 453 setattr(obj, name, attr)
451 454 yield
452 455 setattr(obj, name, orig)
453 456
454 457
455 458 def help_output_test(subcommand=''):
456 459 """test that `ipython [subcommand] -h` works"""
457 460 cmd = get_ipython_cmd() + [subcommand, '-h']
458 461 out, err, rc = get_output_error_code(cmd)
459 462 nt.assert_equal(rc, 0, err)
460 463 nt.assert_not_in("Traceback", err)
461 464 nt.assert_in("Options", out)
462 465 nt.assert_in("--help-all", out)
463 466 return out, err
464 467
465 468
466 469 def help_all_output_test(subcommand=''):
467 470 """test that `ipython [subcommand] --help-all` works"""
468 471 cmd = get_ipython_cmd() + [subcommand, '--help-all']
469 472 out, err, rc = get_output_error_code(cmd)
470 473 nt.assert_equal(rc, 0, err)
471 474 nt.assert_not_in("Traceback", err)
472 475 nt.assert_in("Options", out)
473 476 nt.assert_in("Class parameters", out)
474 477 return out, err
475 478
476 479 def assert_big_text_equal(a, b, chunk_size=80):
477 480 """assert that large strings are equal
478 481
479 482 Zooms in on first chunk that differs,
480 483 to give better info than vanilla assertEqual for large text blobs.
481 484 """
482 485 for i in range(0, len(a), chunk_size):
483 486 chunk_a = a[i:i + chunk_size]
484 487 chunk_b = b[i:i + chunk_size]
485 488 nt.assert_equal(chunk_a, chunk_b, "[offset: %i]\n%r != \n%r" % (
486 489 i, chunk_a, chunk_b))
487 490
488 491 if len(a) > len(b):
489 492 nt.fail("Length doesn't match (%i > %i). Extra text:\n%r" % (
490 493 len(a), len(b), a[len(b):]
491 494 ))
492 495 elif len(a) < len(b):
493 496 nt.fail("Length doesn't match (%i < %i). Extra text:\n%r" % (
494 497 len(a), len(b), b[len(a):]
495 498 ))
This diff has been collapsed as it changes many lines, (1875 lines changed) Show them Hide them
@@ -1,1874 +1,3 b''
1 # encoding: utf-8
2 """
3 A lightweight Traits like module.
1 from __future__ import absolute_import
4 2
5 This is designed to provide a lightweight, simple, pure Python version of
6 many of the capabilities of enthought.traits. This includes:
7
8 * Validation
9 * Type specification with defaults
10 * Static and dynamic notification
11 * Basic predefined types
12 * An API that is similar to enthought.traits
13
14 We don't support:
15
16 * Delegation
17 * Automatic GUI generation
18 * A full set of trait types. Most importantly, we don't provide container
19 traits (list, dict, tuple) that can trigger notifications if their
20 contents change.
21 * API compatibility with enthought.traits
22
23 There are also some important difference in our design:
24
25 * enthought.traits does not validate default values. We do.
26
27 We choose to create this module because we need these capabilities, but
28 we need them to be pure Python so they work in all Python implementations,
29 including Jython and IronPython.
30
31 Inheritance diagram:
32
33 .. inheritance-diagram:: IPython.utils.traitlets
34 :parts: 3
35 """
36
37 # Copyright (c) IPython Development Team.
38 # Distributed under the terms of the Modified BSD License.
39 #
40 # Adapted from enthought.traits, Copyright (c) Enthought, Inc.,
41 # also under the terms of the Modified BSD License.
42
43 import contextlib
44 import inspect
45 import re
46 import sys
47 import types
48 from types import FunctionType
49 try:
50 from types import ClassType, InstanceType
51 ClassTypes = (ClassType, type)
52 except:
53 ClassTypes = (type,)
54 from warnings import warn
55
56 from .getargspec import getargspec
57 from .importstring import import_item
58 from IPython.utils import py3compat
59 from IPython.utils import eventful
60 from IPython.utils.py3compat import iteritems, string_types
61 from IPython.testing.skipdoctest import skip_doctest
62
63 SequenceTypes = (list, tuple, set, frozenset)
64
65 #-----------------------------------------------------------------------------
66 # Basic classes
67 #-----------------------------------------------------------------------------
68
69
70 class NoDefaultSpecified ( object ): pass
71 NoDefaultSpecified = NoDefaultSpecified()
72
73
74 class Undefined ( object ): pass
75 Undefined = Undefined()
76
77 class TraitError(Exception):
78 pass
79
80 #-----------------------------------------------------------------------------
81 # Utilities
82 #-----------------------------------------------------------------------------
83
84
85 def class_of ( object ):
86 """ Returns a string containing the class name of an object with the
87 correct indefinite article ('a' or 'an') preceding it (e.g., 'an Image',
88 'a PlotValue').
89 """
90 if isinstance( object, py3compat.string_types ):
91 return add_article( object )
92
93 return add_article( object.__class__.__name__ )
94
95
96 def add_article ( name ):
97 """ Returns a string containing the correct indefinite article ('a' or 'an')
98 prefixed to the specified string.
99 """
100 if name[:1].lower() in 'aeiou':
101 return 'an ' + name
102
103 return 'a ' + name
104
105
106 def repr_type(obj):
107 """ Return a string representation of a value and its type for readable
108 error messages.
109 """
110 the_type = type(obj)
111 if (not py3compat.PY3) and the_type is InstanceType:
112 # Old-style class.
113 the_type = obj.__class__
114 msg = '%r %r' % (obj, the_type)
115 return msg
116
117
118 def is_trait(t):
119 """ Returns whether the given value is an instance or subclass of TraitType.
120 """
121 return (isinstance(t, TraitType) or
122 (isinstance(t, type) and issubclass(t, TraitType)))
123
124
125 def parse_notifier_name(name):
126 """Convert the name argument to a list of names.
127
128 Examples
129 --------
130
131 >>> parse_notifier_name('a')
132 ['a']
133 >>> parse_notifier_name(['a','b'])
134 ['a', 'b']
135 >>> parse_notifier_name(None)
136 ['anytrait']
137 """
138 if isinstance(name, string_types):
139 return [name]
140 elif name is None:
141 return ['anytrait']
142 elif isinstance(name, (list, tuple)):
143 for n in name:
144 assert isinstance(n, string_types), "names must be strings"
145 return name
146
147
148 class _SimpleTest:
149 def __init__ ( self, value ): self.value = value
150 def __call__ ( self, test ):
151 return test == self.value
152 def __repr__(self):
153 return "<SimpleTest(%r)" % self.value
154 def __str__(self):
155 return self.__repr__()
156
157
158 def getmembers(object, predicate=None):
159 """A safe version of inspect.getmembers that handles missing attributes.
160
161 This is useful when there are descriptor based attributes that for
162 some reason raise AttributeError even though they exist. This happens
163 in zope.inteface with the __provides__ attribute.
164 """
165 results = []
166 for key in dir(object):
167 try:
168 value = getattr(object, key)
169 except AttributeError:
170 pass
171 else:
172 if not predicate or predicate(value):
173 results.append((key, value))
174 results.sort()
175 return results
176
177 def _validate_link(*tuples):
178 """Validate arguments for traitlet link functions"""
179 for t in tuples:
180 if not len(t) == 2:
181 raise TypeError("Each linked traitlet must be specified as (HasTraits, 'trait_name'), not %r" % t)
182 obj, trait_name = t
183 if not isinstance(obj, HasTraits):
184 raise TypeError("Each object must be HasTraits, not %r" % type(obj))
185 if not trait_name in obj.traits():
186 raise TypeError("%r has no trait %r" % (obj, trait_name))
187
188 @skip_doctest
189 class link(object):
190 """Link traits from different objects together so they remain in sync.
191
192 Parameters
193 ----------
194 *args : pairs of objects/attributes
195
196 Examples
197 --------
198
199 >>> c = link((obj1, 'value'), (obj2, 'value'), (obj3, 'value'))
200 >>> obj1.value = 5 # updates other objects as well
201 """
202 updating = False
203 def __init__(self, *args):
204 if len(args) < 2:
205 raise TypeError('At least two traitlets must be provided.')
206 _validate_link(*args)
207
208 self.objects = {}
209
210 initial = getattr(args[0][0], args[0][1])
211 for obj, attr in args:
212 setattr(obj, attr, initial)
213
214 callback = self._make_closure(obj, attr)
215 obj.on_trait_change(callback, attr)
216 self.objects[(obj, attr)] = callback
217
218 @contextlib.contextmanager
219 def _busy_updating(self):
220 self.updating = True
221 try:
222 yield
223 finally:
224 self.updating = False
225
226 def _make_closure(self, sending_obj, sending_attr):
227 def update(name, old, new):
228 self._update(sending_obj, sending_attr, new)
229 return update
230
231 def _update(self, sending_obj, sending_attr, new):
232 if self.updating:
233 return
234 with self._busy_updating():
235 for obj, attr in self.objects.keys():
236 setattr(obj, attr, new)
237
238 def unlink(self):
239 for key, callback in self.objects.items():
240 (obj, attr) = key
241 obj.on_trait_change(callback, attr, remove=True)
242
243 @skip_doctest
244 class directional_link(object):
245 """Link the trait of a source object with traits of target objects.
246
247 Parameters
248 ----------
249 source : pair of object, name
250 targets : pairs of objects/attributes
251
252 Examples
253 --------
254
255 >>> c = directional_link((src, 'value'), (tgt1, 'value'), (tgt2, 'value'))
256 >>> src.value = 5 # updates target objects
257 >>> tgt1.value = 6 # does not update other objects
258 """
259 updating = False
260
261 def __init__(self, source, *targets):
262 if len(targets) < 1:
263 raise TypeError('At least two traitlets must be provided.')
264 _validate_link(source, *targets)
265 self.source = source
266 self.targets = targets
267
268 # Update current value
269 src_attr_value = getattr(source[0], source[1])
270 for obj, attr in targets:
271 setattr(obj, attr, src_attr_value)
272
273 # Wire
274 self.source[0].on_trait_change(self._update, self.source[1])
275
276 @contextlib.contextmanager
277 def _busy_updating(self):
278 self.updating = True
279 try:
280 yield
281 finally:
282 self.updating = False
283
284 def _update(self, name, old, new):
285 if self.updating:
286 return
287 with self._busy_updating():
288 for obj, attr in self.targets:
289 setattr(obj, attr, new)
290
291 def unlink(self):
292 self.source[0].on_trait_change(self._update, self.source[1], remove=True)
293 self.source = None
294 self.targets = []
295
296 dlink = directional_link
297
298
299 #-----------------------------------------------------------------------------
300 # Base TraitType for all traits
301 #-----------------------------------------------------------------------------
302
303
304 class TraitType(object):
305 """A base class for all trait descriptors.
306
307 Notes
308 -----
309 Our implementation of traits is based on Python's descriptor
310 prototol. This class is the base class for all such descriptors. The
311 only magic we use is a custom metaclass for the main :class:`HasTraits`
312 class that does the following:
313
314 1. Sets the :attr:`name` attribute of every :class:`TraitType`
315 instance in the class dict to the name of the attribute.
316 2. Sets the :attr:`this_class` attribute of every :class:`TraitType`
317 instance in the class dict to the *class* that declared the trait.
318 This is used by the :class:`This` trait to allow subclasses to
319 accept superclasses for :class:`This` values.
320 """
321
322 metadata = {}
323 default_value = Undefined
324 allow_none = False
325 info_text = 'any value'
326
327 def __init__(self, default_value=NoDefaultSpecified, allow_none=None, **metadata):
328 """Create a TraitType.
329 """
330 if default_value is not NoDefaultSpecified:
331 self.default_value = default_value
332 if allow_none is not None:
333 self.allow_none = allow_none
334
335 if 'default' in metadata:
336 # Warn the user that they probably meant default_value.
337 warn(
338 "Parameter 'default' passed to TraitType. "
339 "Did you mean 'default_value'?"
340 )
341
342 if len(metadata) > 0:
343 if len(self.metadata) > 0:
344 self._metadata = self.metadata.copy()
345 self._metadata.update(metadata)
346 else:
347 self._metadata = metadata
348 else:
349 self._metadata = self.metadata
350
351 self.init()
352
353 def init(self):
354 pass
355
356 def get_default_value(self):
357 """Create a new instance of the default value."""
358 return self.default_value
359
360 def instance_init(self):
361 """Part of the initialization which may depends on the underlying
362 HasTraits instance.
363
364 It is typically overloaded for specific trait types.
365
366 This method is called by :meth:`HasTraits.__new__` and in the
367 :meth:`TraitType.instance_init` method of trait types holding
368 other trait types.
369 """
370 pass
371
372 def init_default_value(self, obj):
373 """Instantiate the default value for the trait type.
374
375 This method is called by :meth:`TraitType.set_default_value` in the
376 case a default value is provided at construction time or later when
377 accessing the trait value for the first time in
378 :meth:`HasTraits.__get__`.
379 """
380 value = self.get_default_value()
381 value = self._validate(obj, value)
382 obj._trait_values[self.name] = value
383 return value
384
385 def set_default_value(self, obj):
386 """Set the default value on a per instance basis.
387
388 This method is called by :meth:`HasTraits.__new__` to instantiate and
389 validate the default value. The creation and validation of
390 default values must be delayed until the parent :class:`HasTraits`
391 class has been instantiated.
392 Parameters
393 ----------
394 obj : :class:`HasTraits` instance
395 The parent :class:`HasTraits` instance that has just been
396 created.
397 """
398 # Check for a deferred initializer defined in the same class as the
399 # trait declaration or above.
400 mro = type(obj).mro()
401 meth_name = '_%s_default' % self.name
402 for cls in mro[:mro.index(self.this_class)+1]:
403 if meth_name in cls.__dict__:
404 break
405 else:
406 # We didn't find one. Do static initialization.
407 self.init_default_value(obj)
408 return
409 # Complete the dynamic initialization.
410 obj._trait_dyn_inits[self.name] = meth_name
411
412 def __get__(self, obj, cls=None):
413 """Get the value of the trait by self.name for the instance.
414
415 Default values are instantiated when :meth:`HasTraits.__new__`
416 is called. Thus by the time this method gets called either the
417 default value or a user defined value (they called :meth:`__set__`)
418 is in the :class:`HasTraits` instance.
419 """
420 if obj is None:
421 return self
422 else:
423 try:
424 value = obj._trait_values[self.name]
425 except KeyError:
426 # Check for a dynamic initializer.
427 if self.name in obj._trait_dyn_inits:
428 method = getattr(obj, obj._trait_dyn_inits[self.name])
429 value = method()
430 # FIXME: Do we really validate here?
431 value = self._validate(obj, value)
432 obj._trait_values[self.name] = value
433 return value
434 else:
435 return self.init_default_value(obj)
436 except Exception:
437 # HasTraits should call set_default_value to populate
438 # this. So this should never be reached.
439 raise TraitError('Unexpected error in TraitType: '
440 'default value not set properly')
441 else:
442 return value
443
444 def __set__(self, obj, value):
445 new_value = self._validate(obj, value)
446 try:
447 old_value = obj._trait_values[self.name]
448 except KeyError:
449 old_value = Undefined
450
451 obj._trait_values[self.name] = new_value
452 try:
453 silent = bool(old_value == new_value)
454 except:
455 # if there is an error in comparing, default to notify
456 silent = False
457 if silent is not True:
458 # we explicitly compare silent to True just in case the equality
459 # comparison above returns something other than True/False
460 obj._notify_trait(self.name, old_value, new_value)
461
462 def _validate(self, obj, value):
463 if value is None and self.allow_none:
464 return value
465 if hasattr(self, 'validate'):
466 value = self.validate(obj, value)
467 if obj._cross_validation_lock is False:
468 value = self._cross_validate(obj, value)
469 return value
470
471 def _cross_validate(self, obj, value):
472 if hasattr(obj, '_%s_validate' % self.name):
473 cross_validate = getattr(obj, '_%s_validate' % self.name)
474 value = cross_validate(value, self)
475 return value
476
477 def __or__(self, other):
478 if isinstance(other, Union):
479 return Union([self] + other.trait_types)
480 else:
481 return Union([self, other])
482
483 def info(self):
484 return self.info_text
485
486 def error(self, obj, value):
487 if obj is not None:
488 e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
489 % (self.name, class_of(obj),
490 self.info(), repr_type(value))
491 else:
492 e = "The '%s' trait must be %s, but a value of %r was specified." \
493 % (self.name, self.info(), repr_type(value))
494 raise TraitError(e)
495
496 def get_metadata(self, key, default=None):
497 return getattr(self, '_metadata', {}).get(key, default)
498
499 def set_metadata(self, key, value):
500 getattr(self, '_metadata', {})[key] = value
501
502
503 #-----------------------------------------------------------------------------
504 # The HasTraits implementation
505 #-----------------------------------------------------------------------------
506
507
508 class MetaHasTraits(type):
509 """A metaclass for HasTraits.
510
511 This metaclass makes sure that any TraitType class attributes are
512 instantiated and sets their name attribute.
513 """
514
515 def __new__(mcls, name, bases, classdict):
516 """Create the HasTraits class.
517
518 This instantiates all TraitTypes in the class dict and sets their
519 :attr:`name` attribute.
520 """
521 # print "MetaHasTraitlets (mcls, name): ", mcls, name
522 # print "MetaHasTraitlets (bases): ", bases
523 # print "MetaHasTraitlets (classdict): ", classdict
524 for k,v in iteritems(classdict):
525 if isinstance(v, TraitType):
526 v.name = k
527 elif inspect.isclass(v):
528 if issubclass(v, TraitType):
529 vinst = v()
530 vinst.name = k
531 classdict[k] = vinst
532 return super(MetaHasTraits, mcls).__new__(mcls, name, bases, classdict)
533
534 def __init__(cls, name, bases, classdict):
535 """Finish initializing the HasTraits class.
536
537 This sets the :attr:`this_class` attribute of each TraitType in the
538 class dict to the newly created class ``cls``.
539 """
540 for k, v in iteritems(classdict):
541 if isinstance(v, TraitType):
542 v.this_class = cls
543 super(MetaHasTraits, cls).__init__(name, bases, classdict)
544
545
546 class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)):
547
548 def __new__(cls, *args, **kw):
549 # This is needed because object.__new__ only accepts
550 # the cls argument.
551 new_meth = super(HasTraits, cls).__new__
552 if new_meth is object.__new__:
553 inst = new_meth(cls)
554 else:
555 inst = new_meth(cls, **kw)
556 inst._trait_values = {}
557 inst._trait_notifiers = {}
558 inst._trait_dyn_inits = {}
559 inst._cross_validation_lock = True
560 # Here we tell all the TraitType instances to set their default
561 # values on the instance.
562 for key in dir(cls):
563 # Some descriptors raise AttributeError like zope.interface's
564 # __provides__ attributes even though they exist. This causes
565 # AttributeErrors even though they are listed in dir(cls).
566 try:
567 value = getattr(cls, key)
568 except AttributeError:
569 pass
570 else:
571 if isinstance(value, TraitType):
572 value.instance_init()
573 if key not in kw:
574 value.set_default_value(inst)
575 inst._cross_validation_lock = False
576 return inst
577
578 def __init__(self, *args, **kw):
579 # Allow trait values to be set using keyword arguments.
580 # We need to use setattr for this to trigger validation and
581 # notifications.
582 with self.hold_trait_notifications():
583 for key, value in iteritems(kw):
584 setattr(self, key, value)
585
586 @contextlib.contextmanager
587 def hold_trait_notifications(self):
588 """Context manager for bundling trait change notifications and cross
589 validation.
590
591 Use this when doing multiple trait assignments (init, config), to avoid
592 race conditions in trait notifiers requesting other trait values.
593 All trait notifications will fire after all values have been assigned.
594 """
595 if self._cross_validation_lock is True:
596 yield
597 return
598 else:
599 self._cross_validation_lock = True
600 cache = {}
601 notifications = {}
602 _notify_trait = self._notify_trait
603
604 def cache_values(*a):
605 cache[a[0]] = a
606
607 def hold_notifications(*a):
608 notifications[a[0]] = a
609
610 self._notify_trait = cache_values
611
612 try:
613 yield
614 finally:
615 try:
616 self._notify_trait = hold_notifications
617 for name in cache:
618 if hasattr(self, '_%s_validate' % name):
619 cross_validate = getattr(self, '_%s_validate' % name)
620 setattr(self, name, cross_validate(getattr(self, name), self))
621 except TraitError as e:
622 self._notify_trait = lambda *x: None
623 for name in cache:
624 if cache[name][1] is not Undefined:
625 setattr(self, name, cache[name][1])
626 else:
627 delattr(self, name)
628 cache = {}
629 notifications = {}
630 raise e
631 finally:
632 self._notify_trait = _notify_trait
633 self._cross_validation_lock = False
634 if isinstance(_notify_trait, types.MethodType):
635 # FIXME: remove when support is bumped to 3.4.
636 # when original method is restored,
637 # remove the redundant value from __dict__
638 # (only used to preserve pickleability on Python < 3.4)
639 self.__dict__.pop('_notify_trait', None)
640 # trigger delayed notifications
641 for v in dict(cache, **notifications).values():
642 self._notify_trait(*v)
643
644 def _notify_trait(self, name, old_value, new_value):
645
646 # First dynamic ones
647 callables = []
648 callables.extend(self._trait_notifiers.get(name,[]))
649 callables.extend(self._trait_notifiers.get('anytrait',[]))
650
651 # Now static ones
652 try:
653 cb = getattr(self, '_%s_changed' % name)
654 except:
655 pass
656 else:
657 callables.append(cb)
658
659 # Call them all now
660 for c in callables:
661 # Traits catches and logs errors here. I allow them to raise
662 if callable(c):
663 argspec = getargspec(c)
664
665 nargs = len(argspec[0])
666 # Bound methods have an additional 'self' argument
667 # I don't know how to treat unbound methods, but they
668 # can't really be used for callbacks.
669 if isinstance(c, types.MethodType):
670 offset = -1
671 else:
672 offset = 0
673 if nargs + offset == 0:
674 c()
675 elif nargs + offset == 1:
676 c(name)
677 elif nargs + offset == 2:
678 c(name, new_value)
679 elif nargs + offset == 3:
680 c(name, old_value, new_value)
681 else:
682 raise TraitError('a trait changed callback '
683 'must have 0-3 arguments.')
684 else:
685 raise TraitError('a trait changed callback '
686 'must be callable.')
687
688
689 def _add_notifiers(self, handler, name):
690 if name not in self._trait_notifiers:
691 nlist = []
692 self._trait_notifiers[name] = nlist
693 else:
694 nlist = self._trait_notifiers[name]
695 if handler not in nlist:
696 nlist.append(handler)
697
698 def _remove_notifiers(self, handler, name):
699 if name in self._trait_notifiers:
700 nlist = self._trait_notifiers[name]
701 try:
702 index = nlist.index(handler)
703 except ValueError:
704 pass
705 else:
706 del nlist[index]
707
708 def on_trait_change(self, handler, name=None, remove=False):
709 """Setup a handler to be called when a trait changes.
710
711 This is used to setup dynamic notifications of trait changes.
712
713 Static handlers can be created by creating methods on a HasTraits
714 subclass with the naming convention '_[traitname]_changed'. Thus,
715 to create static handler for the trait 'a', create the method
716 _a_changed(self, name, old, new) (fewer arguments can be used, see
717 below).
718
719 Parameters
720 ----------
721 handler : callable
722 A callable that is called when a trait changes. Its
723 signature can be handler(), handler(name), handler(name, new)
724 or handler(name, old, new).
725 name : list, str, None
726 If None, the handler will apply to all traits. If a list
727 of str, handler will apply to all names in the list. If a
728 str, the handler will apply just to that name.
729 remove : bool
730 If False (the default), then install the handler. If True
731 then unintall it.
732 """
733 if remove:
734 names = parse_notifier_name(name)
735 for n in names:
736 self._remove_notifiers(handler, n)
737 else:
738 names = parse_notifier_name(name)
739 for n in names:
740 self._add_notifiers(handler, n)
741
742 @classmethod
743 def class_trait_names(cls, **metadata):
744 """Get a list of all the names of this class' traits.
745
746 This method is just like the :meth:`trait_names` method,
747 but is unbound.
748 """
749 return cls.class_traits(**metadata).keys()
750
751 @classmethod
752 def class_traits(cls, **metadata):
753 """Get a `dict` of all the traits of this class. The dictionary
754 is keyed on the name and the values are the TraitType objects.
755
756 This method is just like the :meth:`traits` method, but is unbound.
757
758 The TraitTypes returned don't know anything about the values
759 that the various HasTrait's instances are holding.
760
761 The metadata kwargs allow functions to be passed in which
762 filter traits based on metadata values. The functions should
763 take a single value as an argument and return a boolean. If
764 any function returns False, then the trait is not included in
765 the output. This does not allow for any simple way of
766 testing that a metadata name exists and has any
767 value because get_metadata returns None if a metadata key
768 doesn't exist.
769 """
770 traits = dict([memb for memb in getmembers(cls) if
771 isinstance(memb[1], TraitType)])
772
773 if len(metadata) == 0:
774 return traits
775
776 for meta_name, meta_eval in metadata.items():
777 if type(meta_eval) is not FunctionType:
778 metadata[meta_name] = _SimpleTest(meta_eval)
779
780 result = {}
781 for name, trait in traits.items():
782 for meta_name, meta_eval in metadata.items():
783 if not meta_eval(trait.get_metadata(meta_name)):
784 break
785 else:
786 result[name] = trait
787
788 return result
789
790 def trait_names(self, **metadata):
791 """Get a list of all the names of this class' traits."""
792 return self.traits(**metadata).keys()
793
794 def traits(self, **metadata):
795 """Get a `dict` of all the traits of this class. The dictionary
796 is keyed on the name and the values are the TraitType objects.
797
798 The TraitTypes returned don't know anything about the values
799 that the various HasTrait's instances are holding.
800
801 The metadata kwargs allow functions to be passed in which
802 filter traits based on metadata values. The functions should
803 take a single value as an argument and return a boolean. If
804 any function returns False, then the trait is not included in
805 the output. This does not allow for any simple way of
806 testing that a metadata name exists and has any
807 value because get_metadata returns None if a metadata key
808 doesn't exist.
809 """
810 traits = dict([memb for memb in getmembers(self.__class__) if
811 isinstance(memb[1], TraitType)])
812
813 if len(metadata) == 0:
814 return traits
815
816 for meta_name, meta_eval in metadata.items():
817 if type(meta_eval) is not FunctionType:
818 metadata[meta_name] = _SimpleTest(meta_eval)
819
820 result = {}
821 for name, trait in traits.items():
822 for meta_name, meta_eval in metadata.items():
823 if not meta_eval(trait.get_metadata(meta_name)):
824 break
825 else:
826 result[name] = trait
827
828 return result
829
830 def trait_metadata(self, traitname, key, default=None):
831 """Get metadata values for trait by key."""
832 try:
833 trait = getattr(self.__class__, traitname)
834 except AttributeError:
835 raise TraitError("Class %s does not have a trait named %s" %
836 (self.__class__.__name__, traitname))
837 else:
838 return trait.get_metadata(key, default)
839
840 def add_trait(self, traitname, trait):
841 """Dynamically add a trait attribute to the HasTraits instance."""
842 self.__class__ = type(self.__class__.__name__, (self.__class__,),
843 {traitname: trait})
844 trait.set_default_value(self)
845
846 #-----------------------------------------------------------------------------
847 # Actual TraitTypes implementations/subclasses
848 #-----------------------------------------------------------------------------
849
850 #-----------------------------------------------------------------------------
851 # TraitTypes subclasses for handling classes and instances of classes
852 #-----------------------------------------------------------------------------
853
854
855 class ClassBasedTraitType(TraitType):
856 """
857 A trait with error reporting and string -> type resolution for Type,
858 Instance and This.
859 """
860
861 def _resolve_string(self, string):
862 """
863 Resolve a string supplied for a type into an actual object.
864 """
865 return import_item(string)
866
867 def error(self, obj, value):
868 kind = type(value)
869 if (not py3compat.PY3) and kind is InstanceType:
870 msg = 'class %s' % value.__class__.__name__
871 else:
872 msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) )
873
874 if obj is not None:
875 e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \
876 % (self.name, class_of(obj),
877 self.info(), msg)
878 else:
879 e = "The '%s' trait must be %s, but a value of %r was specified." \
880 % (self.name, self.info(), msg)
881
882 raise TraitError(e)
883
884
885 class Type(ClassBasedTraitType):
886 """A trait whose value must be a subclass of a specified class."""
887
888 def __init__ (self, default_value=None, klass=None, allow_none=False,
889 **metadata):
890 """Construct a Type trait
891
892 A Type trait specifies that its values must be subclasses of
893 a particular class.
894
895 If only ``default_value`` is given, it is used for the ``klass`` as
896 well.
897
898 Parameters
899 ----------
900 default_value : class, str or None
901 The default value must be a subclass of klass. If an str,
902 the str must be a fully specified class name, like 'foo.bar.Bah'.
903 The string is resolved into real class, when the parent
904 :class:`HasTraits` class is instantiated.
905 klass : class, str, None
906 Values of this trait must be a subclass of klass. The klass
907 may be specified in a string like: 'foo.bar.MyClass'.
908 The string is resolved into real class, when the parent
909 :class:`HasTraits` class is instantiated.
910 allow_none : bool [ default True ]
911 Indicates whether None is allowed as an assignable value. Even if
912 ``False``, the default value may be ``None``.
913 """
914 if default_value is None:
915 if klass is None:
916 klass = object
917 elif klass is None:
918 klass = default_value
919
920 if not (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)):
921 raise TraitError("A Type trait must specify a class.")
922
923 self.klass = klass
924
925 super(Type, self).__init__(default_value, allow_none=allow_none, **metadata)
926
927 def validate(self, obj, value):
928 """Validates that the value is a valid object instance."""
929 if isinstance(value, py3compat.string_types):
930 try:
931 value = self._resolve_string(value)
932 except ImportError:
933 raise TraitError("The '%s' trait of %s instance must be a type, but "
934 "%r could not be imported" % (self.name, obj, value))
935 try:
936 if issubclass(value, self.klass):
937 return value
938 except:
939 pass
940
941 self.error(obj, value)
942
943 def info(self):
944 """ Returns a description of the trait."""
945 if isinstance(self.klass, py3compat.string_types):
946 klass = self.klass
947 else:
948 klass = self.klass.__name__
949 result = 'a subclass of ' + klass
950 if self.allow_none:
951 return result + ' or None'
952 return result
953
954 def instance_init(self):
955 self._resolve_classes()
956 super(Type, self).instance_init()
957
958 def _resolve_classes(self):
959 if isinstance(self.klass, py3compat.string_types):
960 self.klass = self._resolve_string(self.klass)
961 if isinstance(self.default_value, py3compat.string_types):
962 self.default_value = self._resolve_string(self.default_value)
963
964 def get_default_value(self):
965 return self.default_value
966
967
968 class DefaultValueGenerator(object):
969 """A class for generating new default value instances."""
970
971 def __init__(self, *args, **kw):
972 self.args = args
973 self.kw = kw
974
975 def generate(self, klass):
976 return klass(*self.args, **self.kw)
977
978
979 class Instance(ClassBasedTraitType):
980 """A trait whose value must be an instance of a specified class.
981
982 The value can also be an instance of a subclass of the specified class.
983
984 Subclasses can declare default classes by overriding the klass attribute
985 """
986
987 klass = None
988
989 def __init__(self, klass=None, args=None, kw=None, allow_none=False,
990 **metadata ):
991 """Construct an Instance trait.
992
993 This trait allows values that are instances of a particular
994 class or its subclasses. Our implementation is quite different
995 from that of enthough.traits as we don't allow instances to be used
996 for klass and we handle the ``args`` and ``kw`` arguments differently.
997
998 Parameters
999 ----------
1000 klass : class, str
1001 The class that forms the basis for the trait. Class names
1002 can also be specified as strings, like 'foo.bar.Bar'.
1003 args : tuple
1004 Positional arguments for generating the default value.
1005 kw : dict
1006 Keyword arguments for generating the default value.
1007 allow_none : bool [default True]
1008 Indicates whether None is allowed as a value.
1009
1010 Notes
1011 -----
1012 If both ``args`` and ``kw`` are None, then the default value is None.
1013 If ``args`` is a tuple and ``kw`` is a dict, then the default is
1014 created as ``klass(*args, **kw)``. If exactly one of ``args`` or ``kw`` is
1015 None, the None is replaced by ``()`` or ``{}``, respectively.
1016 """
1017 if klass is None:
1018 klass = self.klass
1019
1020 if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)):
1021 self.klass = klass
1022 else:
1023 raise TraitError('The klass attribute must be a class'
1024 ' not: %r' % klass)
1025
1026 # self.klass is a class, so handle default_value
1027 if args is None and kw is None:
1028 default_value = None
1029 else:
1030 if args is None:
1031 # kw is not None
1032 args = ()
1033 elif kw is None:
1034 # args is not None
1035 kw = {}
1036
1037 if not isinstance(kw, dict):
1038 raise TraitError("The 'kw' argument must be a dict or None.")
1039 if not isinstance(args, tuple):
1040 raise TraitError("The 'args' argument must be a tuple or None.")
1041
1042 default_value = DefaultValueGenerator(*args, **kw)
1043
1044 super(Instance, self).__init__(default_value, allow_none=allow_none, **metadata)
1045
1046 def validate(self, obj, value):
1047 if isinstance(value, self.klass):
1048 return value
1049 else:
1050 self.error(obj, value)
1051
1052 def info(self):
1053 if isinstance(self.klass, py3compat.string_types):
1054 klass = self.klass
1055 else:
1056 klass = self.klass.__name__
1057 result = class_of(klass)
1058 if self.allow_none:
1059 return result + ' or None'
1060
1061 return result
1062
1063 def instance_init(self):
1064 self._resolve_classes()
1065 super(Instance, self).instance_init()
1066
1067 def _resolve_classes(self):
1068 if isinstance(self.klass, py3compat.string_types):
1069 self.klass = self._resolve_string(self.klass)
1070
1071 def get_default_value(self):
1072 """Instantiate a default value instance.
1073
1074 This is called when the containing HasTraits classes'
1075 :meth:`__new__` method is called to ensure that a unique instance
1076 is created for each HasTraits instance.
1077 """
1078 dv = self.default_value
1079 if isinstance(dv, DefaultValueGenerator):
1080 return dv.generate(self.klass)
1081 else:
1082 return dv
1083
1084
1085 class ForwardDeclaredMixin(object):
1086 """
1087 Mixin for forward-declared versions of Instance and Type.
1088 """
1089 def _resolve_string(self, string):
1090 """
1091 Find the specified class name by looking for it in the module in which
1092 our this_class attribute was defined.
1093 """
1094 modname = self.this_class.__module__
1095 return import_item('.'.join([modname, string]))
1096
1097
1098 class ForwardDeclaredType(ForwardDeclaredMixin, Type):
1099 """
1100 Forward-declared version of Type.
1101 """
1102 pass
1103
1104
1105 class ForwardDeclaredInstance(ForwardDeclaredMixin, Instance):
1106 """
1107 Forward-declared version of Instance.
1108 """
1109 pass
1110
1111
1112 class This(ClassBasedTraitType):
1113 """A trait for instances of the class containing this trait.
1114
1115 Because how how and when class bodies are executed, the ``This``
1116 trait can only have a default value of None. This, and because we
1117 always validate default values, ``allow_none`` is *always* true.
1118 """
1119
1120 info_text = 'an instance of the same type as the receiver or None'
1121
1122 def __init__(self, **metadata):
1123 super(This, self).__init__(None, **metadata)
1124
1125 def validate(self, obj, value):
1126 # What if value is a superclass of obj.__class__? This is
1127 # complicated if it was the superclass that defined the This
1128 # trait.
1129 if isinstance(value, self.this_class) or (value is None):
1130 return value
1131 else:
1132 self.error(obj, value)
1133
1134
1135 class Union(TraitType):
1136 """A trait type representing a Union type."""
1137
1138 def __init__(self, trait_types, **metadata):
1139 """Construct a Union trait.
1140
1141 This trait allows values that are allowed by at least one of the
1142 specified trait types. A Union traitlet cannot have metadata on
1143 its own, besides the metadata of the listed types.
1144
1145 Parameters
1146 ----------
1147 trait_types: sequence
1148 The list of trait types of length at least 1.
1149
1150 Notes
1151 -----
1152 Union([Float(), Bool(), Int()]) attempts to validate the provided values
1153 with the validation function of Float, then Bool, and finally Int.
1154 """
1155 self.trait_types = trait_types
1156 self.info_text = " or ".join([tt.info_text for tt in self.trait_types])
1157 self.default_value = self.trait_types[0].get_default_value()
1158 super(Union, self).__init__(**metadata)
1159
1160 def instance_init(self):
1161 for trait_type in self.trait_types:
1162 trait_type.name = self.name
1163 trait_type.this_class = self.this_class
1164 trait_type.instance_init()
1165 super(Union, self).instance_init()
1166
1167 def validate(self, obj, value):
1168 for trait_type in self.trait_types:
1169 try:
1170 v = trait_type._validate(obj, value)
1171 self._metadata = trait_type._metadata
1172 return v
1173 except TraitError:
1174 continue
1175 self.error(obj, value)
1176
1177 def __or__(self, other):
1178 if isinstance(other, Union):
1179 return Union(self.trait_types + other.trait_types)
1180 else:
1181 return Union(self.trait_types + [other])
1182
1183 #-----------------------------------------------------------------------------
1184 # Basic TraitTypes implementations/subclasses
1185 #-----------------------------------------------------------------------------
1186
1187
1188 class Any(TraitType):
1189 default_value = None
1190 info_text = 'any value'
1191
1192
1193 class Int(TraitType):
1194 """An int trait."""
1195
1196 default_value = 0
1197 info_text = 'an int'
1198
1199 def validate(self, obj, value):
1200 if isinstance(value, int):
1201 return value
1202 self.error(obj, value)
1203
1204 class CInt(Int):
1205 """A casting version of the int trait."""
1206
1207 def validate(self, obj, value):
1208 try:
1209 return int(value)
1210 except:
1211 self.error(obj, value)
1212
1213 if py3compat.PY3:
1214 Long, CLong = Int, CInt
1215 Integer = Int
1216 else:
1217 class Long(TraitType):
1218 """A long integer trait."""
1219
1220 default_value = 0
1221 info_text = 'a long'
1222
1223 def validate(self, obj, value):
1224 if isinstance(value, long):
1225 return value
1226 if isinstance(value, int):
1227 return long(value)
1228 self.error(obj, value)
1229
1230
1231 class CLong(Long):
1232 """A casting version of the long integer trait."""
1233
1234 def validate(self, obj, value):
1235 try:
1236 return long(value)
1237 except:
1238 self.error(obj, value)
1239
1240 class Integer(TraitType):
1241 """An integer trait.
1242
1243 Longs that are unnecessary (<= sys.maxint) are cast to ints."""
1244
1245 default_value = 0
1246 info_text = 'an integer'
1247
1248 def validate(self, obj, value):
1249 if isinstance(value, int):
1250 return value
1251 if isinstance(value, long):
1252 # downcast longs that fit in int:
1253 # note that int(n > sys.maxint) returns a long, so
1254 # we don't need a condition on this cast
1255 return int(value)
1256 if sys.platform == "cli":
1257 from System import Int64
1258 if isinstance(value, Int64):
1259 return int(value)
1260 self.error(obj, value)
1261
1262
1263 class Float(TraitType):
1264 """A float trait."""
1265
1266 default_value = 0.0
1267 info_text = 'a float'
1268
1269 def validate(self, obj, value):
1270 if isinstance(value, float):
1271 return value
1272 if isinstance(value, int):
1273 return float(value)
1274 self.error(obj, value)
1275
1276
1277 class CFloat(Float):
1278 """A casting version of the float trait."""
1279
1280 def validate(self, obj, value):
1281 try:
1282 return float(value)
1283 except:
1284 self.error(obj, value)
1285
1286 class Complex(TraitType):
1287 """A trait for complex numbers."""
1288
1289 default_value = 0.0 + 0.0j
1290 info_text = 'a complex number'
1291
1292 def validate(self, obj, value):
1293 if isinstance(value, complex):
1294 return value
1295 if isinstance(value, (float, int)):
1296 return complex(value)
1297 self.error(obj, value)
1298
1299
1300 class CComplex(Complex):
1301 """A casting version of the complex number trait."""
1302
1303 def validate (self, obj, value):
1304 try:
1305 return complex(value)
1306 except:
1307 self.error(obj, value)
1308
1309 # We should always be explicit about whether we're using bytes or unicode, both
1310 # for Python 3 conversion and for reliable unicode behaviour on Python 2. So
1311 # we don't have a Str type.
1312 class Bytes(TraitType):
1313 """A trait for byte strings."""
1314
1315 default_value = b''
1316 info_text = 'a bytes object'
1317
1318 def validate(self, obj, value):
1319 if isinstance(value, bytes):
1320 return value
1321 self.error(obj, value)
1322
1323
1324 class CBytes(Bytes):
1325 """A casting version of the byte string trait."""
1326
1327 def validate(self, obj, value):
1328 try:
1329 return bytes(value)
1330 except:
1331 self.error(obj, value)
1332
1333
1334 class Unicode(TraitType):
1335 """A trait for unicode strings."""
1336
1337 default_value = u''
1338 info_text = 'a unicode string'
1339
1340 def validate(self, obj, value):
1341 if isinstance(value, py3compat.unicode_type):
1342 return value
1343 if isinstance(value, bytes):
1344 try:
1345 return value.decode('ascii', 'strict')
1346 except UnicodeDecodeError:
1347 msg = "Could not decode {!r} for unicode trait '{}' of {} instance."
1348 raise TraitError(msg.format(value, self.name, class_of(obj)))
1349 self.error(obj, value)
1350
1351
1352 class CUnicode(Unicode):
1353 """A casting version of the unicode trait."""
1354
1355 def validate(self, obj, value):
1356 try:
1357 return py3compat.unicode_type(value)
1358 except:
1359 self.error(obj, value)
1360
1361
1362 class ObjectName(TraitType):
1363 """A string holding a valid object name in this version of Python.
1364
1365 This does not check that the name exists in any scope."""
1366 info_text = "a valid object identifier in Python"
1367
1368 if py3compat.PY3:
1369 # Python 3:
1370 coerce_str = staticmethod(lambda _,s: s)
1371
1372 else:
1373 # Python 2:
1374 def coerce_str(self, obj, value):
1375 "In Python 2, coerce ascii-only unicode to str"
1376 if isinstance(value, unicode):
1377 try:
1378 return str(value)
1379 except UnicodeEncodeError:
1380 self.error(obj, value)
1381 return value
1382
1383 def validate(self, obj, value):
1384 value = self.coerce_str(obj, value)
1385
1386 if isinstance(value, string_types) and py3compat.isidentifier(value):
1387 return value
1388 self.error(obj, value)
1389
1390 class DottedObjectName(ObjectName):
1391 """A string holding a valid dotted object name in Python, such as A.b3._c"""
1392 def validate(self, obj, value):
1393 value = self.coerce_str(obj, value)
1394
1395 if isinstance(value, string_types) and py3compat.isidentifier(value, dotted=True):
1396 return value
1397 self.error(obj, value)
1398
1399
1400 class Bool(TraitType):
1401 """A boolean (True, False) trait."""
1402
1403 default_value = False
1404 info_text = 'a boolean'
1405
1406 def validate(self, obj, value):
1407 if isinstance(value, bool):
1408 return value
1409 self.error(obj, value)
1410
1411
1412 class CBool(Bool):
1413 """A casting version of the boolean trait."""
1414
1415 def validate(self, obj, value):
1416 try:
1417 return bool(value)
1418 except:
1419 self.error(obj, value)
1420
1421
1422 class Enum(TraitType):
1423 """An enum that whose value must be in a given sequence."""
1424
1425 def __init__(self, values, default_value=None, **metadata):
1426 self.values = values
1427 super(Enum, self).__init__(default_value, **metadata)
1428
1429 def validate(self, obj, value):
1430 if value in self.values:
1431 return value
1432 self.error(obj, value)
1433
1434 def info(self):
1435 """ Returns a description of the trait."""
1436 result = 'any of ' + repr(self.values)
1437 if self.allow_none:
1438 return result + ' or None'
1439 return result
1440
1441 class CaselessStrEnum(Enum):
1442 """An enum of strings that are caseless in validate."""
1443
1444 def validate(self, obj, value):
1445 if not isinstance(value, py3compat.string_types):
1446 self.error(obj, value)
1447
1448 for v in self.values:
1449 if v.lower() == value.lower():
1450 return v
1451 self.error(obj, value)
1452
1453 class Container(Instance):
1454 """An instance of a container (list, set, etc.)
1455
1456 To be subclassed by overriding klass.
1457 """
1458 klass = None
1459 _cast_types = ()
1460 _valid_defaults = SequenceTypes
1461 _trait = None
1462
1463 def __init__(self, trait=None, default_value=None, allow_none=False,
1464 **metadata):
1465 """Create a container trait type from a list, set, or tuple.
1466
1467 The default value is created by doing ``List(default_value)``,
1468 which creates a copy of the ``default_value``.
1469
1470 ``trait`` can be specified, which restricts the type of elements
1471 in the container to that TraitType.
1472
1473 If only one arg is given and it is not a Trait, it is taken as
1474 ``default_value``:
1475
1476 ``c = List([1,2,3])``
1477
1478 Parameters
1479 ----------
1480
1481 trait : TraitType [ optional ]
1482 the type for restricting the contents of the Container. If unspecified,
1483 types are not checked.
1484
1485 default_value : SequenceType [ optional ]
1486 The default value for the Trait. Must be list/tuple/set, and
1487 will be cast to the container type.
1488
1489 allow_none : bool [ default False ]
1490 Whether to allow the value to be None
1491
1492 **metadata : any
1493 further keys for extensions to the Trait (e.g. config)
1494
1495 """
1496 # allow List([values]):
1497 if default_value is None and not is_trait(trait):
1498 default_value = trait
1499 trait = None
1500
1501 if default_value is None:
1502 args = ()
1503 elif isinstance(default_value, self._valid_defaults):
1504 args = (default_value,)
1505 else:
1506 raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
1507
1508 if is_trait(trait):
1509 self._trait = trait() if isinstance(trait, type) else trait
1510 self._trait.name = 'element'
1511 elif trait is not None:
1512 raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait))
1513
1514 super(Container,self).__init__(klass=self.klass, args=args,
1515 allow_none=allow_none, **metadata)
1516
1517 def element_error(self, obj, element, validator):
1518 e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \
1519 % (self.name, class_of(obj), validator.info(), repr_type(element))
1520 raise TraitError(e)
1521
1522 def validate(self, obj, value):
1523 if isinstance(value, self._cast_types):
1524 value = self.klass(value)
1525 value = super(Container, self).validate(obj, value)
1526 if value is None:
1527 return value
1528
1529 value = self.validate_elements(obj, value)
1530
1531 return value
1532
1533 def validate_elements(self, obj, value):
1534 validated = []
1535 if self._trait is None or isinstance(self._trait, Any):
1536 return value
1537 for v in value:
1538 try:
1539 v = self._trait._validate(obj, v)
1540 except TraitError:
1541 self.element_error(obj, v, self._trait)
1542 else:
1543 validated.append(v)
1544 return self.klass(validated)
1545
1546 def instance_init(self):
1547 if isinstance(self._trait, TraitType):
1548 self._trait.this_class = self.this_class
1549 self._trait.instance_init()
1550 super(Container, self).instance_init()
1551
1552
1553 class List(Container):
1554 """An instance of a Python list."""
1555 klass = list
1556 _cast_types = (tuple,)
1557
1558 def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, **metadata):
1559 """Create a List trait type from a list, set, or tuple.
1560
1561 The default value is created by doing ``List(default_value)``,
1562 which creates a copy of the ``default_value``.
1563
1564 ``trait`` can be specified, which restricts the type of elements
1565 in the container to that TraitType.
1566
1567 If only one arg is given and it is not a Trait, it is taken as
1568 ``default_value``:
1569
1570 ``c = List([1,2,3])``
1571
1572 Parameters
1573 ----------
1574
1575 trait : TraitType [ optional ]
1576 the type for restricting the contents of the Container. If unspecified,
1577 types are not checked.
1578
1579 default_value : SequenceType [ optional ]
1580 The default value for the Trait. Must be list/tuple/set, and
1581 will be cast to the container type.
1582
1583 minlen : Int [ default 0 ]
1584 The minimum length of the input list
1585
1586 maxlen : Int [ default sys.maxsize ]
1587 The maximum length of the input list
1588
1589 allow_none : bool [ default False ]
1590 Whether to allow the value to be None
1591
1592 **metadata : any
1593 further keys for extensions to the Trait (e.g. config)
1594
1595 """
1596 self._minlen = minlen
1597 self._maxlen = maxlen
1598 super(List, self).__init__(trait=trait, default_value=default_value,
1599 **metadata)
1600
1601 def length_error(self, obj, value):
1602 e = "The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified." \
1603 % (self.name, class_of(obj), self._minlen, self._maxlen, value)
1604 raise TraitError(e)
1605
1606 def validate_elements(self, obj, value):
1607 length = len(value)
1608 if length < self._minlen or length > self._maxlen:
1609 self.length_error(obj, value)
1610
1611 return super(List, self).validate_elements(obj, value)
1612
1613 def validate(self, obj, value):
1614 value = super(List, self).validate(obj, value)
1615 value = self.validate_elements(obj, value)
1616 return value
1617
1618
1619 class Set(List):
1620 """An instance of a Python set."""
1621 klass = set
1622 _cast_types = (tuple, list)
1623
1624
1625 class Tuple(Container):
1626 """An instance of a Python tuple."""
1627 klass = tuple
1628 _cast_types = (list,)
1629
1630 def __init__(self, *traits, **metadata):
1631 """Tuple(*traits, default_value=None, **medatata)
1632
1633 Create a tuple from a list, set, or tuple.
1634
1635 Create a fixed-type tuple with Traits:
1636
1637 ``t = Tuple(Int, Str, CStr)``
1638
1639 would be length 3, with Int,Str,CStr for each element.
1640
1641 If only one arg is given and it is not a Trait, it is taken as
1642 default_value:
1643
1644 ``t = Tuple((1,2,3))``
1645
1646 Otherwise, ``default_value`` *must* be specified by keyword.
1647
1648 Parameters
1649 ----------
1650
1651 *traits : TraitTypes [ optional ]
1652 the types for restricting the contents of the Tuple. If unspecified,
1653 types are not checked. If specified, then each positional argument
1654 corresponds to an element of the tuple. Tuples defined with traits
1655 are of fixed length.
1656
1657 default_value : SequenceType [ optional ]
1658 The default value for the Tuple. Must be list/tuple/set, and
1659 will be cast to a tuple. If `traits` are specified, the
1660 `default_value` must conform to the shape and type they specify.
1661
1662 allow_none : bool [ default False ]
1663 Whether to allow the value to be None
1664
1665 **metadata : any
1666 further keys for extensions to the Trait (e.g. config)
1667
1668 """
1669 default_value = metadata.pop('default_value', None)
1670 allow_none = metadata.pop('allow_none', True)
1671
1672 # allow Tuple((values,)):
1673 if len(traits) == 1 and default_value is None and not is_trait(traits[0]):
1674 default_value = traits[0]
1675 traits = ()
1676
1677 if default_value is None:
1678 args = ()
1679 elif isinstance(default_value, self._valid_defaults):
1680 args = (default_value,)
1681 else:
1682 raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
1683
1684 self._traits = []
1685 for trait in traits:
1686 t = trait() if isinstance(trait, type) else trait
1687 t.name = 'element'
1688 self._traits.append(t)
1689
1690 if self._traits and default_value is None:
1691 # don't allow default to be an empty container if length is specified
1692 args = None
1693 super(Container,self).__init__(klass=self.klass, args=args, allow_none=allow_none, **metadata)
1694
1695 def validate_elements(self, obj, value):
1696 if not self._traits:
1697 # nothing to validate
1698 return value
1699 if len(value) != len(self._traits):
1700 e = "The '%s' trait of %s instance requires %i elements, but a value of %s was specified." \
1701 % (self.name, class_of(obj), len(self._traits), repr_type(value))
1702 raise TraitError(e)
1703
1704 validated = []
1705 for t, v in zip(self._traits, value):
1706 try:
1707 v = t._validate(obj, v)
1708 except TraitError:
1709 self.element_error(obj, v, t)
1710 else:
1711 validated.append(v)
1712 return tuple(validated)
1713
1714 def instance_init(self):
1715 for trait in self._traits:
1716 if isinstance(trait, TraitType):
1717 trait.this_class = self.this_class
1718 trait.instance_init()
1719 super(Container, self).instance_init()
1720
1721
1722 class Dict(Instance):
1723 """An instance of a Python dict."""
1724 _trait = None
1725
1726 def __init__(self, trait=None, default_value=NoDefaultSpecified, allow_none=False, **metadata):
1727 """Create a dict trait type from a dict.
1728
1729 The default value is created by doing ``dict(default_value)``,
1730 which creates a copy of the ``default_value``.
1731
1732 trait : TraitType [ optional ]
1733 the type for restricting the contents of the Container. If unspecified,
1734 types are not checked.
1735
1736 default_value : SequenceType [ optional ]
1737 The default value for the Dict. Must be dict, tuple, or None, and
1738 will be cast to a dict if not None. If `trait` is specified, the
1739 `default_value` must conform to the constraints it specifies.
1740
1741 allow_none : bool [ default False ]
1742 Whether to allow the value to be None
1743
1744 """
1745 if default_value is NoDefaultSpecified and trait is not None:
1746 if not is_trait(trait):
1747 default_value = trait
1748 trait = None
1749 if default_value is NoDefaultSpecified:
1750 default_value = {}
1751 if default_value is None:
1752 args = None
1753 elif isinstance(default_value, dict):
1754 args = (default_value,)
1755 elif isinstance(default_value, SequenceTypes):
1756 args = (default_value,)
1757 else:
1758 raise TypeError('default value of Dict was %s' % default_value)
1759
1760 if is_trait(trait):
1761 self._trait = trait() if isinstance(trait, type) else trait
1762 self._trait.name = 'element'
1763 elif trait is not None:
1764 raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait))
1765
1766 super(Dict,self).__init__(klass=dict, args=args,
1767 allow_none=allow_none, **metadata)
1768
1769 def element_error(self, obj, element, validator):
1770 e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \
1771 % (self.name, class_of(obj), validator.info(), repr_type(element))
1772 raise TraitError(e)
1773
1774 def validate(self, obj, value):
1775 value = super(Dict, self).validate(obj, value)
1776 if value is None:
1777 return value
1778 value = self.validate_elements(obj, value)
1779 return value
1780
1781 def validate_elements(self, obj, value):
1782 if self._trait is None or isinstance(self._trait, Any):
1783 return value
1784 validated = {}
1785 for key in value:
1786 v = value[key]
1787 try:
1788 v = self._trait._validate(obj, v)
1789 except TraitError:
1790 self.element_error(obj, v, self._trait)
1791 else:
1792 validated[key] = v
1793 return self.klass(validated)
1794
1795 def instance_init(self):
1796 if isinstance(self._trait, TraitType):
1797 self._trait.this_class = self.this_class
1798 self._trait.instance_init()
1799 super(Dict, self).instance_init()
1800
1801
1802 class EventfulDict(Instance):
1803 """An instance of an EventfulDict."""
1804
1805 def __init__(self, default_value={}, allow_none=False, **metadata):
1806 """Create a EventfulDict trait type from a dict.
1807
1808 The default value is created by doing
1809 ``eventful.EvenfulDict(default_value)``, which creates a copy of the
1810 ``default_value``.
1811 """
1812 if default_value is None:
1813 args = None
1814 elif isinstance(default_value, dict):
1815 args = (default_value,)
1816 elif isinstance(default_value, SequenceTypes):
1817 args = (default_value,)
1818 else:
1819 raise TypeError('default value of EventfulDict was %s' % default_value)
1820
1821 super(EventfulDict, self).__init__(klass=eventful.EventfulDict, args=args,
1822 allow_none=allow_none, **metadata)
1823
1824
1825 class EventfulList(Instance):
1826 """An instance of an EventfulList."""
1827
1828 def __init__(self, default_value=None, allow_none=False, **metadata):
1829 """Create a EventfulList trait type from a dict.
1830
1831 The default value is created by doing
1832 ``eventful.EvenfulList(default_value)``, which creates a copy of the
1833 ``default_value``.
1834 """
1835 if default_value is None:
1836 args = ((),)
1837 else:
1838 args = (default_value,)
1839
1840 super(EventfulList, self).__init__(klass=eventful.EventfulList, args=args,
1841 allow_none=allow_none, **metadata)
1842
1843
1844 class TCPAddress(TraitType):
1845 """A trait for an (ip, port) tuple.
1846
1847 This allows for both IPv4 IP addresses as well as hostnames.
1848 """
1849
1850 default_value = ('127.0.0.1', 0)
1851 info_text = 'an (ip, port) tuple'
1852
1853 def validate(self, obj, value):
1854 if isinstance(value, tuple):
1855 if len(value) == 2:
1856 if isinstance(value[0], py3compat.string_types) and isinstance(value[1], int):
1857 port = value[1]
1858 if port >= 0 and port <= 65535:
1859 return value
1860 self.error(obj, value)
1861
1862 class CRegExp(TraitType):
1863 """A casting compiled regular expression trait.
1864
1865 Accepts both strings and compiled regular expressions. The resulting
1866 attribute will be a compiled regular expression."""
1867
1868 info_text = 'a regular expression'
1869
1870 def validate(self, obj, value):
1871 try:
1872 return re.compile(value)
1873 except:
1874 self.error(obj, value)
3 from traitlets import *
1 NO CONTENT: file renamed from IPython/config/__init__.py to traitlets/config/__init__.py
@@ -1,622 +1,622 b''
1 1 # encoding: utf-8
2 2 """A base class for a configurable application."""
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 json
10 10 import logging
11 11 import os
12 12 import re
13 13 import sys
14 14 from copy import deepcopy
15 15 from collections import defaultdict
16 16
17 17 from decorator import decorator
18 18
19 from IPython.config.configurable import SingletonConfigurable
20 from IPython.config.loader import (
19 from traitlets.config.configurable import SingletonConfigurable
20 from traitlets.config.loader import (
21 21 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, JSONFileConfigLoader
22 22 )
23 23
24 from IPython.utils.traitlets import (
24 from traitlets.traitlets import (
25 25 Unicode, List, Enum, Dict, Instance, TraitError
26 26 )
27 27 from IPython.utils.importstring import import_item
28 28 from IPython.utils.text import indent, wrap_paragraphs, dedent
29 29 from IPython.utils import py3compat
30 30 from IPython.utils.py3compat import string_types, iteritems
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Descriptions for the various sections
34 34 #-----------------------------------------------------------------------------
35 35
36 36 # merge flags&aliases into options
37 37 option_description = """
38 38 Arguments that take values are actually convenience aliases to full
39 39 Configurables, whose aliases are listed on the help line. For more information
40 40 on full configurables, see '--help-all'.
41 41 """.strip() # trim newlines of front and back
42 42
43 43 keyvalue_description = """
44 44 Parameters are set from command-line arguments of the form:
45 45 `--Class.trait=value`.
46 46 This line is evaluated in Python, so simple expressions are allowed, e.g.::
47 47 `--C.a='range(3)'` For setting C.a=[0,1,2].
48 48 """.strip() # trim newlines of front and back
49 49
50 50 # sys.argv can be missing, for example when python is embedded. See the docs
51 51 # for details: http://docs.python.org/2/c-api/intro.html#embedding-python
52 52 if not hasattr(sys, "argv"):
53 53 sys.argv = [""]
54 54
55 55 subcommand_description = """
56 56 Subcommands are launched as `{app} cmd [args]`. For information on using
57 57 subcommand 'cmd', do: `{app} cmd -h`.
58 58 """
59 59 # get running program name
60 60
61 61 #-----------------------------------------------------------------------------
62 62 # Application class
63 63 #-----------------------------------------------------------------------------
64 64
65 65 @decorator
66 66 def catch_config_error(method, app, *args, **kwargs):
67 67 """Method decorator for catching invalid config (Trait/ArgumentErrors) during init.
68 68
69 69 On a TraitError (generally caused by bad config), this will print the trait's
70 70 message, and exit the app.
71 71
72 72 For use on init methods, to prevent invoking excepthook on invalid input.
73 73 """
74 74 try:
75 75 return method(app, *args, **kwargs)
76 76 except (TraitError, ArgumentError) as e:
77 77 app.print_help()
78 78 app.log.fatal("Bad config encountered during initialization:")
79 79 app.log.fatal(str(e))
80 80 app.log.debug("Config at the time: %s", app.config)
81 81 app.exit(1)
82 82
83 83
84 84 class ApplicationError(Exception):
85 85 pass
86 86
87 87 class LevelFormatter(logging.Formatter):
88 88 """Formatter with additional `highlevel` record
89 89
90 90 This field is empty if log level is less than highlevel_limit,
91 91 otherwise it is formatted with self.highlevel_format.
92 92
93 93 Useful for adding 'WARNING' to warning messages,
94 94 without adding 'INFO' to info, etc.
95 95 """
96 96 highlevel_limit = logging.WARN
97 97 highlevel_format = " %(levelname)s |"
98 98
99 99 def format(self, record):
100 100 if record.levelno >= self.highlevel_limit:
101 101 record.highlevel = self.highlevel_format % record.__dict__
102 102 else:
103 103 record.highlevel = ""
104 104 return super(LevelFormatter, self).format(record)
105 105
106 106
107 107 class Application(SingletonConfigurable):
108 108 """A singleton application with full configuration support."""
109 109
110 110 # The name of the application, will usually match the name of the command
111 111 # line application
112 112 name = Unicode(u'application')
113 113
114 114 # The description of the application that is printed at the beginning
115 115 # of the help.
116 116 description = Unicode(u'This is an application.')
117 117 # default section descriptions
118 118 option_description = Unicode(option_description)
119 119 keyvalue_description = Unicode(keyvalue_description)
120 120 subcommand_description = Unicode(subcommand_description)
121 121
122 122 python_config_loader_class = PyFileConfigLoader
123 123 json_config_loader_class = JSONFileConfigLoader
124 124
125 125 # The usage and example string that goes at the end of the help string.
126 126 examples = Unicode()
127 127
128 128 # A sequence of Configurable subclasses whose config=True attributes will
129 129 # be exposed at the command line.
130 130 classes = []
131 131 @property
132 132 def _help_classes(self):
133 133 """Define `App.help_classes` if CLI classes should differ from config file classes"""
134 134 return getattr(self, 'help_classes', self.classes)
135 135
136 136 @property
137 137 def _config_classes(self):
138 138 """Define `App.config_classes` if config file classes should differ from CLI classes."""
139 139 return getattr(self, 'config_classes', self.classes)
140 140
141 141 # The version string of this application.
142 142 version = Unicode(u'0.0')
143 143
144 144 # the argv used to initialize the application
145 145 argv = List()
146 146
147 147 # The log level for the application
148 148 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
149 149 default_value=logging.WARN,
150 150 config=True,
151 151 help="Set the log level by value or name.")
152 152 def _log_level_changed(self, name, old, new):
153 153 """Adjust the log level when log_level is set."""
154 154 if isinstance(new, string_types):
155 155 new = getattr(logging, new)
156 156 self.log_level = new
157 157 self.log.setLevel(new)
158 158
159 159 _log_formatter_cls = LevelFormatter
160 160
161 161 log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", config=True,
162 162 help="The date format used by logging formatters for %(asctime)s"
163 163 )
164 164 def _log_datefmt_changed(self, name, old, new):
165 165 self._log_format_changed('log_format', self.log_format, self.log_format)
166 166
167 167 log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True,
168 168 help="The Logging format template",
169 169 )
170 170 def _log_format_changed(self, name, old, new):
171 171 """Change the log formatter when log_format is set."""
172 172 _log_handler = self.log.handlers[0]
173 173 _log_formatter = self._log_formatter_cls(fmt=new, datefmt=self.log_datefmt)
174 174 _log_handler.setFormatter(_log_formatter)
175 175
176 176
177 177 log = Instance(logging.Logger)
178 178 def _log_default(self):
179 179 """Start logging for this application.
180 180
181 181 The default is to log to stderr using a StreamHandler, if no default
182 182 handler already exists. The log level starts at logging.WARN, but this
183 183 can be adjusted by setting the ``log_level`` attribute.
184 184 """
185 185 log = logging.getLogger(self.__class__.__name__)
186 186 log.setLevel(self.log_level)
187 187 log.propagate = False
188 188 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
189 189 while _log:
190 190 if _log.handlers:
191 191 return log
192 192 if not _log.propagate:
193 193 break
194 194 else:
195 195 _log = _log.parent
196 196 if sys.executable.endswith('pythonw.exe'):
197 197 # this should really go to a file, but file-logging is only
198 198 # hooked up in parallel applications
199 199 _log_handler = logging.StreamHandler(open(os.devnull, 'w'))
200 200 else:
201 201 _log_handler = logging.StreamHandler()
202 202 _log_formatter = self._log_formatter_cls(fmt=self.log_format, datefmt=self.log_datefmt)
203 203 _log_handler.setFormatter(_log_formatter)
204 204 log.addHandler(_log_handler)
205 205 return log
206 206
207 207 # the alias map for configurables
208 208 aliases = Dict({'log-level' : 'Application.log_level'})
209 209
210 210 # flags for loading Configurables or store_const style flags
211 211 # flags are loaded from this dict by '--key' flags
212 212 # this must be a dict of two-tuples, the first element being the Config/dict
213 213 # and the second being the help string for the flag
214 214 flags = Dict()
215 215 def _flags_changed(self, name, old, new):
216 216 """ensure flags dict is valid"""
217 217 for key,value in iteritems(new):
218 218 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
219 219 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
220 220 assert isinstance(value[1], string_types), "Bad flag: %r:%s"%(key,value)
221 221
222 222
223 223 # subcommands for launching other applications
224 224 # if this is not empty, this will be a parent Application
225 225 # this must be a dict of two-tuples,
226 226 # the first element being the application class/import string
227 227 # and the second being the help string for the subcommand
228 228 subcommands = Dict()
229 229 # parse_command_line will initialize a subapp, if requested
230 subapp = Instance('IPython.config.application.Application', allow_none=True)
230 subapp = Instance('traitlets.config.application.Application', allow_none=True)
231 231
232 232 # extra command-line arguments that don't set config values
233 233 extra_args = List(Unicode)
234 234
235 235
236 236 def __init__(self, **kwargs):
237 237 SingletonConfigurable.__init__(self, **kwargs)
238 238 # Ensure my class is in self.classes, so my attributes appear in command line
239 239 # options and config files.
240 240 if self.__class__ not in self.classes:
241 241 self.classes.insert(0, self.__class__)
242 242
243 243 def _config_changed(self, name, old, new):
244 244 SingletonConfigurable._config_changed(self, name, old, new)
245 245 self.log.debug('Config changed:')
246 246 self.log.debug(repr(new))
247 247
248 248 @catch_config_error
249 249 def initialize(self, argv=None):
250 250 """Do the basic steps to configure me.
251 251
252 252 Override in subclasses.
253 253 """
254 254 self.parse_command_line(argv)
255 255
256 256
257 257 def start(self):
258 258 """Start the app mainloop.
259 259
260 260 Override in subclasses.
261 261 """
262 262 if self.subapp is not None:
263 263 return self.subapp.start()
264 264
265 265 def print_alias_help(self):
266 266 """Print the alias part of the help."""
267 267 if not self.aliases:
268 268 return
269 269
270 270 lines = []
271 271 classdict = {}
272 272 for cls in self._help_classes:
273 273 # include all parents (up to, but excluding Configurable) in available names
274 274 for c in cls.mro()[:-3]:
275 275 classdict[c.__name__] = c
276 276
277 277 for alias, longname in iteritems(self.aliases):
278 278 classname, traitname = longname.split('.',1)
279 279 cls = classdict[classname]
280 280
281 281 trait = cls.class_traits(config=True)[traitname]
282 282 help = cls.class_get_trait_help(trait).splitlines()
283 283 # reformat first line
284 284 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
285 285 if len(alias) == 1:
286 286 help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
287 287 lines.extend(help)
288 288 # lines.append('')
289 289 print(os.linesep.join(lines))
290 290
291 291 def print_flag_help(self):
292 292 """Print the flag part of the help."""
293 293 if not self.flags:
294 294 return
295 295
296 296 lines = []
297 297 for m, (cfg,help) in iteritems(self.flags):
298 298 prefix = '--' if len(m) > 1 else '-'
299 299 lines.append(prefix+m)
300 300 lines.append(indent(dedent(help.strip())))
301 301 # lines.append('')
302 302 print(os.linesep.join(lines))
303 303
304 304 def print_options(self):
305 305 if not self.flags and not self.aliases:
306 306 return
307 307 lines = ['Options']
308 308 lines.append('-'*len(lines[0]))
309 309 lines.append('')
310 310 for p in wrap_paragraphs(self.option_description):
311 311 lines.append(p)
312 312 lines.append('')
313 313 print(os.linesep.join(lines))
314 314 self.print_flag_help()
315 315 self.print_alias_help()
316 316 print()
317 317
318 318 def print_subcommands(self):
319 319 """Print the subcommand part of the help."""
320 320 if not self.subcommands:
321 321 return
322 322
323 323 lines = ["Subcommands"]
324 324 lines.append('-'*len(lines[0]))
325 325 lines.append('')
326 326 for p in wrap_paragraphs(self.subcommand_description.format(
327 327 app=self.name)):
328 328 lines.append(p)
329 329 lines.append('')
330 330 for subc, (cls, help) in iteritems(self.subcommands):
331 331 lines.append(subc)
332 332 if help:
333 333 lines.append(indent(dedent(help.strip())))
334 334 lines.append('')
335 335 print(os.linesep.join(lines))
336 336
337 337 def print_help(self, classes=False):
338 338 """Print the help for each Configurable class in self.classes.
339 339
340 340 If classes=False (the default), only flags and aliases are printed.
341 341 """
342 342 self.print_description()
343 343 self.print_subcommands()
344 344 self.print_options()
345 345
346 346 if classes:
347 347 help_classes = self._help_classes
348 348 if help_classes:
349 349 print("Class parameters")
350 350 print("----------------")
351 351 print()
352 352 for p in wrap_paragraphs(self.keyvalue_description):
353 353 print(p)
354 354 print()
355 355
356 356 for cls in help_classes:
357 357 cls.class_print_help()
358 358 print()
359 359 else:
360 360 print("To see all available configurables, use `--help-all`")
361 361 print()
362 362
363 363 self.print_examples()
364 364
365 365
366 366 def print_description(self):
367 367 """Print the application description."""
368 368 for p in wrap_paragraphs(self.description):
369 369 print(p)
370 370 print()
371 371
372 372 def print_examples(self):
373 373 """Print usage and examples.
374 374
375 375 This usage string goes at the end of the command line help string
376 376 and should contain examples of the application's usage.
377 377 """
378 378 if self.examples:
379 379 print("Examples")
380 380 print("--------")
381 381 print()
382 382 print(indent(dedent(self.examples.strip())))
383 383 print()
384 384
385 385 def print_version(self):
386 386 """Print the version string."""
387 387 print(self.version)
388 388
389 389 def update_config(self, config):
390 390 """Fire the traits events when the config is updated."""
391 391 # Save a copy of the current config.
392 392 newconfig = deepcopy(self.config)
393 393 # Merge the new config into the current one.
394 394 newconfig.merge(config)
395 395 # Save the combined config as self.config, which triggers the traits
396 396 # events.
397 397 self.config = newconfig
398 398
399 399 @catch_config_error
400 400 def initialize_subcommand(self, subc, argv=None):
401 401 """Initialize a subcommand with argv."""
402 402 subapp,help = self.subcommands.get(subc)
403 403
404 404 if isinstance(subapp, string_types):
405 405 subapp = import_item(subapp)
406 406
407 407 # clear existing instances
408 408 self.__class__.clear_instance()
409 409 # instantiate
410 410 self.subapp = subapp.instance(config=self.config)
411 411 # and initialize subapp
412 412 self.subapp.initialize(argv)
413 413
414 414 def flatten_flags(self):
415 415 """flatten flags and aliases, so cl-args override as expected.
416 416
417 417 This prevents issues such as an alias pointing to InteractiveShell,
418 418 but a config file setting the same trait in TerminalInteraciveShell
419 419 getting inappropriate priority over the command-line arg.
420 420
421 421 Only aliases with exactly one descendent in the class list
422 422 will be promoted.
423 423
424 424 """
425 425 # build a tree of classes in our list that inherit from a particular
426 426 # it will be a dict by parent classname of classes in our list
427 427 # that are descendents
428 428 mro_tree = defaultdict(list)
429 429 for cls in self._help_classes:
430 430 clsname = cls.__name__
431 431 for parent in cls.mro()[1:-3]:
432 432 # exclude cls itself and Configurable,HasTraits,object
433 433 mro_tree[parent.__name__].append(clsname)
434 434 # flatten aliases, which have the form:
435 435 # { 'alias' : 'Class.trait' }
436 436 aliases = {}
437 437 for alias, cls_trait in iteritems(self.aliases):
438 438 cls,trait = cls_trait.split('.',1)
439 439 children = mro_tree[cls]
440 440 if len(children) == 1:
441 441 # exactly one descendent, promote alias
442 442 cls = children[0]
443 443 aliases[alias] = '.'.join([cls,trait])
444 444
445 445 # flatten flags, which are of the form:
446 446 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
447 447 flags = {}
448 448 for key, (flagdict, help) in iteritems(self.flags):
449 449 newflag = {}
450 450 for cls, subdict in iteritems(flagdict):
451 451 children = mro_tree[cls]
452 452 # exactly one descendent, promote flag section
453 453 if len(children) == 1:
454 454 cls = children[0]
455 455 newflag[cls] = subdict
456 456 flags[key] = (newflag, help)
457 457 return flags, aliases
458 458
459 459 @catch_config_error
460 460 def parse_command_line(self, argv=None):
461 461 """Parse the command line arguments."""
462 462 argv = sys.argv[1:] if argv is None else argv
463 463 self.argv = [ py3compat.cast_unicode(arg) for arg in argv ]
464 464
465 465 if argv and argv[0] == 'help':
466 466 # turn `ipython help notebook` into `ipython notebook -h`
467 467 argv = argv[1:] + ['-h']
468 468
469 469 if self.subcommands and len(argv) > 0:
470 470 # we have subcommands, and one may have been specified
471 471 subc, subargv = argv[0], argv[1:]
472 472 if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands:
473 473 # it's a subcommand, and *not* a flag or class parameter
474 474 return self.initialize_subcommand(subc, subargv)
475 475
476 476 # Arguments after a '--' argument are for the script IPython may be
477 477 # about to run, not IPython iteslf. For arguments parsed here (help and
478 478 # version), we want to only search the arguments up to the first
479 479 # occurrence of '--', which we're calling interpreted_argv.
480 480 try:
481 481 interpreted_argv = argv[:argv.index('--')]
482 482 except ValueError:
483 483 interpreted_argv = argv
484 484
485 485 if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')):
486 486 self.print_help('--help-all' in interpreted_argv)
487 487 self.exit(0)
488 488
489 489 if '--version' in interpreted_argv or '-V' in interpreted_argv:
490 490 self.print_version()
491 491 self.exit(0)
492 492
493 493 # flatten flags&aliases, so cl-args get appropriate priority:
494 494 flags,aliases = self.flatten_flags()
495 495 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
496 496 flags=flags, log=self.log)
497 497 config = loader.load_config()
498 498 self.update_config(config)
499 499 # store unparsed args in extra_args
500 500 self.extra_args = loader.extra_args
501 501
502 502 @classmethod
503 503 def _load_config_files(cls, basefilename, path=None, log=None):
504 504 """Load config files (py,json) by filename and path.
505 505
506 506 yield each config object in turn.
507 507 """
508 508
509 509 if not isinstance(path, list):
510 510 path = [path]
511 511 for path in path[::-1]:
512 512 # path list is in descending priority order, so load files backwards:
513 513 pyloader = cls.python_config_loader_class(basefilename+'.py', path=path, log=log)
514 514 jsonloader = cls.json_config_loader_class(basefilename+'.json', path=path, log=log)
515 515 config = None
516 516 for loader in [pyloader, jsonloader]:
517 517 try:
518 518 config = loader.load_config()
519 519 except ConfigFileNotFound:
520 520 pass
521 521 except Exception:
522 522 # try to get the full filename, but it will be empty in the
523 523 # unlikely event that the error raised before filefind finished
524 524 filename = loader.full_filename or basefilename
525 525 # problem while running the file
526 526 if log:
527 527 log.error("Exception while loading config file %s",
528 528 filename, exc_info=True)
529 529 else:
530 530 if log:
531 531 log.debug("Loaded config file: %s", loader.full_filename)
532 532 if config:
533 533 yield config
534 534
535 535 raise StopIteration
536 536
537 537
538 538 @catch_config_error
539 539 def load_config_file(self, filename, path=None):
540 540 """Load config files by filename and path."""
541 541 filename, ext = os.path.splitext(filename)
542 542 loaded = []
543 543 for config in self._load_config_files(filename, path=path, log=self.log):
544 544 loaded.append(config)
545 545 self.update_config(config)
546 546 if len(loaded) > 1:
547 547 collisions = loaded[0].collisions(loaded[1])
548 548 if collisions:
549 549 self.log.warn("Collisions detected in {0}.py and {0}.json config files."
550 550 " {0}.json has higher priority: {1}".format(
551 551 filename, json.dumps(collisions, indent=2),
552 552 ))
553 553
554 554
555 555 def generate_config_file(self):
556 556 """generate default config file from Configurables"""
557 557 lines = ["# Configuration file for %s." % self.name]
558 558 lines.append('')
559 559 for cls in self._config_classes:
560 560 lines.append(cls.class_config_section())
561 561 return '\n'.join(lines)
562 562
563 563 def exit(self, exit_status=0):
564 564 self.log.debug("Exiting application: %s" % self.name)
565 565 sys.exit(exit_status)
566 566
567 567 @classmethod
568 568 def launch_instance(cls, argv=None, **kwargs):
569 569 """Launch a global instance of this Application
570 570
571 571 If a global instance already exists, this reinitializes and starts it
572 572 """
573 573 app = cls.instance(**kwargs)
574 574 app.initialize(argv)
575 575 app.start()
576 576
577 577 #-----------------------------------------------------------------------------
578 578 # utility functions, for convenience
579 579 #-----------------------------------------------------------------------------
580 580
581 581 def boolean_flag(name, configurable, set_help='', unset_help=''):
582 582 """Helper for building basic --trait, --no-trait flags.
583 583
584 584 Parameters
585 585 ----------
586 586
587 587 name : str
588 588 The name of the flag.
589 589 configurable : str
590 590 The 'Class.trait' string of the trait to be set/unset with the flag
591 591 set_help : unicode
592 592 help string for --name flag
593 593 unset_help : unicode
594 594 help string for --no-name flag
595 595
596 596 Returns
597 597 -------
598 598
599 599 cfg : dict
600 600 A dict with two keys: 'name', and 'no-name', for setting and unsetting
601 601 the trait, respectively.
602 602 """
603 603 # default helpstrings
604 604 set_help = set_help or "set %s=True"%configurable
605 605 unset_help = unset_help or "set %s=False"%configurable
606 606
607 607 cls,trait = configurable.split('.')
608 608
609 609 setter = {cls : {trait : True}}
610 610 unsetter = {cls : {trait : False}}
611 611 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
612 612
613 613
614 614 def get_config():
615 615 """Get the config object for the global Application instance, if there is one
616 616
617 617 otherwise return an empty config object
618 618 """
619 619 if Application.initialized():
620 620 return Application.instance().config
621 621 else:
622 622 return Config()
@@ -1,380 +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 from IPython.utils.traitlets import HasTraits, Instance
13 from traitlets.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 parent = Instance('IPython.config.configurable.Configurable', allow_none=True)
37 parent = Instance('traitlets.config.configurable.Configurable', allow_none=True)
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 141
142 142 # hold trait notifications until after all config has been loaded
143 143 with self.hold_trait_notifications():
144 144 for name, config_value in iteritems(my_config):
145 145 if name in traits:
146 146 if isinstance(config_value, LazyConfigValue):
147 147 # ConfigValue is a wrapper for using append / update on containers
148 148 # without having to copy the initial value
149 149 initial = getattr(self, name)
150 150 config_value = config_value.get_value(initial)
151 151 # We have to do a deepcopy here if we don't deepcopy the entire
152 152 # config object. If we don't, a mutable config_value will be
153 153 # shared by all instances, effectively making it a class attribute.
154 154 setattr(self, name, deepcopy(config_value))
155 155
156 156 def _config_changed(self, name, old, new):
157 157 """Update all the class traits having ``config=True`` as metadata.
158 158
159 159 For any class trait with a ``config`` metadata attribute that is
160 160 ``True``, we update the trait with the value of the corresponding
161 161 config entry.
162 162 """
163 163 # Get all traits with a config metadata entry that is True
164 164 traits = self.traits(config=True)
165 165
166 166 # We auto-load config section for this class as well as any parent
167 167 # classes that are Configurable subclasses. This starts with Configurable
168 168 # and works down the mro loading the config for each section.
169 169 section_names = self.section_names()
170 170 self._load_config(new, traits=traits, section_names=section_names)
171 171
172 172 def update_config(self, config):
173 173 """Fire the traits events when the config is updated."""
174 174 # Save a copy of the current config.
175 175 newconfig = deepcopy(self.config)
176 176 # Merge the new config into the current one.
177 177 newconfig.merge(config)
178 178 # Save the combined config as self.config, which triggers the traits
179 179 # events.
180 180 self.config = newconfig
181 181
182 182 @classmethod
183 183 def class_get_help(cls, inst=None):
184 184 """Get the help string for this class in ReST format.
185 185
186 186 If `inst` is given, it's current trait values will be used in place of
187 187 class defaults.
188 188 """
189 189 assert inst is None or isinstance(inst, cls)
190 190 final_help = []
191 191 final_help.append(u'%s options' % cls.__name__)
192 192 final_help.append(len(final_help[0])*u'-')
193 193 for k, v in sorted(cls.class_traits(config=True).items()):
194 194 help = cls.class_get_trait_help(v, inst)
195 195 final_help.append(help)
196 196 return '\n'.join(final_help)
197 197
198 198 @classmethod
199 199 def class_get_trait_help(cls, trait, inst=None):
200 200 """Get the help string for a single trait.
201 201
202 202 If `inst` is given, it's current trait values will be used in place of
203 203 the class default.
204 204 """
205 205 assert inst is None or isinstance(inst, cls)
206 206 lines = []
207 207 header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
208 208 lines.append(header)
209 209 if inst is not None:
210 210 lines.append(indent('Current: %r' % getattr(inst, trait.name), 4))
211 211 else:
212 212 try:
213 213 dvr = repr(trait.get_default_value())
214 214 except Exception:
215 215 dvr = None # ignore defaults we can't construct
216 216 if dvr is not None:
217 217 if len(dvr) > 64:
218 218 dvr = dvr[:61]+'...'
219 219 lines.append(indent('Default: %s' % dvr, 4))
220 220 if 'Enum' in trait.__class__.__name__:
221 221 # include Enum choices
222 222 lines.append(indent('Choices: %r' % (trait.values,)))
223 223
224 224 help = trait.get_metadata('help')
225 225 if help is not None:
226 226 help = '\n'.join(wrap_paragraphs(help, 76))
227 227 lines.append(indent(help, 4))
228 228 return '\n'.join(lines)
229 229
230 230 @classmethod
231 231 def class_print_help(cls, inst=None):
232 232 """Get the help string for a single trait and print it."""
233 233 print(cls.class_get_help(inst))
234 234
235 235 @classmethod
236 236 def class_config_section(cls):
237 237 """Get the config class config section"""
238 238 def c(s):
239 239 """return a commented, wrapped block."""
240 240 s = '\n\n'.join(wrap_paragraphs(s, 78))
241 241
242 242 return '# ' + s.replace('\n', '\n# ')
243 243
244 244 # section header
245 245 breaker = '#' + '-'*78
246 246 s = "# %s configuration" % cls.__name__
247 247 lines = [breaker, s, breaker, '']
248 248 # get the description trait
249 249 desc = cls.class_traits().get('description')
250 250 if desc:
251 251 desc = desc.default_value
252 252 else:
253 253 # no description trait, use __doc__
254 254 desc = getattr(cls, '__doc__', '')
255 255 if desc:
256 256 lines.append(c(desc))
257 257 lines.append('')
258 258
259 259 parents = []
260 260 for parent in cls.mro():
261 261 # only include parents that are not base classes
262 262 # and are not the class itself
263 263 # and have some configurable traits to inherit
264 264 if parent is not cls and issubclass(parent, Configurable) and \
265 265 parent.class_traits(config=True):
266 266 parents.append(parent)
267 267
268 268 if parents:
269 269 pstr = ', '.join([ p.__name__ for p in parents ])
270 270 lines.append(c('%s will inherit config from: %s'%(cls.__name__, pstr)))
271 271 lines.append('')
272 272
273 273 for name, trait in iteritems(cls.class_traits(config=True)):
274 274 help = trait.get_metadata('help') or ''
275 275 lines.append(c(help))
276 276 lines.append('# c.%s.%s = %r'%(cls.__name__, name, trait.get_default_value()))
277 277 lines.append('')
278 278 return '\n'.join(lines)
279 279
280 280
281 281
282 282 class SingletonConfigurable(Configurable):
283 283 """A configurable that only allows one instance.
284 284
285 285 This class is for classes that should only have one instance of itself
286 286 or *any* subclass. To create and retrieve such a class use the
287 287 :meth:`SingletonConfigurable.instance` method.
288 288 """
289 289
290 290 _instance = None
291 291
292 292 @classmethod
293 293 def _walk_mro(cls):
294 294 """Walk the cls.mro() for parent classes that are also singletons
295 295
296 296 For use in instance()
297 297 """
298 298
299 299 for subclass in cls.mro():
300 300 if issubclass(cls, subclass) and \
301 301 issubclass(subclass, SingletonConfigurable) and \
302 302 subclass != SingletonConfigurable:
303 303 yield subclass
304 304
305 305 @classmethod
306 306 def clear_instance(cls):
307 307 """unset _instance for this class and singleton parents.
308 308 """
309 309 if not cls.initialized():
310 310 return
311 311 for subclass in cls._walk_mro():
312 312 if isinstance(subclass._instance, cls):
313 313 # only clear instances that are instances
314 314 # of the calling class
315 315 subclass._instance = None
316 316
317 317 @classmethod
318 318 def instance(cls, *args, **kwargs):
319 319 """Returns a global instance of this class.
320 320
321 321 This method create a new instance if none have previously been created
322 322 and returns a previously created instance is one already exists.
323 323
324 324 The arguments and keyword arguments passed to this method are passed
325 325 on to the :meth:`__init__` method of the class upon instantiation.
326 326
327 327 Examples
328 328 --------
329 329
330 330 Create a singleton class using instance, and retrieve it::
331 331
332 >>> from IPython.config.configurable import SingletonConfigurable
332 >>> from traitlets.config.configurable import SingletonConfigurable
333 333 >>> class Foo(SingletonConfigurable): pass
334 334 >>> foo = Foo.instance()
335 335 >>> foo == Foo.instance()
336 336 True
337 337
338 338 Create a subclass that is retrived using the base class instance::
339 339
340 340 >>> class Bar(SingletonConfigurable): pass
341 341 >>> class Bam(Bar): pass
342 342 >>> bam = Bam.instance()
343 343 >>> bam == Bar.instance()
344 344 True
345 345 """
346 346 # Create and save the instance
347 347 if cls._instance is None:
348 348 inst = cls(*args, **kwargs)
349 349 # Now make sure that the instance will also be returned by
350 350 # parent classes' _instance attribute.
351 351 for subclass in cls._walk_mro():
352 352 subclass._instance = inst
353 353
354 354 if isinstance(cls._instance, cls):
355 355 return cls._instance
356 356 else:
357 357 raise MultipleInstanceError(
358 358 'Multiple incompatible subclass instances of '
359 359 '%s are being created.' % cls.__name__
360 360 )
361 361
362 362 @classmethod
363 363 def initialized(cls):
364 364 """Has an instance been created?"""
365 365 return hasattr(cls, "_instance") and cls._instance is not None
366 366
367 367
368 368 class LoggingConfigurable(Configurable):
369 369 """A parent class for Configurables that log.
370 370
371 371 Subclasses have a log trait, and the default behavior
372 372 is to get the logger from the currently running Application.
373 373 """
374 374
375 375 log = Instance('logging.Logger')
376 376 def _log_default(self):
377 377 from IPython.utils import log
378 378 return log.get_logger()
379 379
380 380
@@ -1,837 +1,837 b''
1 1 # encoding: utf-8
2 2 """A simple configuration system."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import argparse
8 8 import copy
9 9 import logging
10 10 import os
11 11 import re
12 12 import sys
13 13 import json
14 14 from ast import literal_eval
15 15
16 16 from IPython.utils.path import filefind, get_ipython_dir
17 17 from IPython.utils import py3compat
18 18 from IPython.utils.encoding import DEFAULT_ENCODING
19 19 from IPython.utils.py3compat import unicode_type, iteritems
20 from IPython.utils.traitlets import HasTraits, List, Any
20 from traitlets.traitlets import HasTraits, List, Any
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Exceptions
24 24 #-----------------------------------------------------------------------------
25 25
26 26
27 27 class ConfigError(Exception):
28 28 pass
29 29
30 30 class ConfigLoaderError(ConfigError):
31 31 pass
32 32
33 33 class ConfigFileNotFound(ConfigError):
34 34 pass
35 35
36 36 class ArgumentError(ConfigLoaderError):
37 37 pass
38 38
39 39 #-----------------------------------------------------------------------------
40 40 # Argparse fix
41 41 #-----------------------------------------------------------------------------
42 42
43 43 # Unfortunately argparse by default prints help messages to stderr instead of
44 44 # stdout. This makes it annoying to capture long help screens at the command
45 45 # line, since one must know how to pipe stderr, which many users don't know how
46 46 # to do. So we override the print_help method with one that defaults to
47 47 # stdout and use our class instead.
48 48
49 49 class ArgumentParser(argparse.ArgumentParser):
50 50 """Simple argparse subclass that prints help to stdout by default."""
51 51
52 52 def print_help(self, file=None):
53 53 if file is None:
54 54 file = sys.stdout
55 55 return super(ArgumentParser, self).print_help(file)
56 56
57 57 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
58 58
59 59 #-----------------------------------------------------------------------------
60 60 # Config class for holding config information
61 61 #-----------------------------------------------------------------------------
62 62
63 63 class LazyConfigValue(HasTraits):
64 64 """Proxy object for exposing methods on configurable containers
65 65
66 66 Exposes:
67 67
68 68 - append, extend, insert on lists
69 69 - update on dicts
70 70 - update, add on sets
71 71 """
72 72
73 73 _value = None
74 74
75 75 # list methods
76 76 _extend = List()
77 77 _prepend = List()
78 78
79 79 def append(self, obj):
80 80 self._extend.append(obj)
81 81
82 82 def extend(self, other):
83 83 self._extend.extend(other)
84 84
85 85 def prepend(self, other):
86 86 """like list.extend, but for the front"""
87 87 self._prepend[:0] = other
88 88
89 89 _inserts = List()
90 90 def insert(self, index, other):
91 91 if not isinstance(index, int):
92 92 raise TypeError("An integer is required")
93 93 self._inserts.append((index, other))
94 94
95 95 # dict methods
96 96 # update is used for both dict and set
97 97 _update = Any()
98 98 def update(self, other):
99 99 if self._update is None:
100 100 if isinstance(other, dict):
101 101 self._update = {}
102 102 else:
103 103 self._update = set()
104 104 self._update.update(other)
105 105
106 106 # set methods
107 107 def add(self, obj):
108 108 self.update({obj})
109 109
110 110 def get_value(self, initial):
111 111 """construct the value from the initial one
112 112
113 113 after applying any insert / extend / update changes
114 114 """
115 115 if self._value is not None:
116 116 return self._value
117 117 value = copy.deepcopy(initial)
118 118 if isinstance(value, list):
119 119 for idx, obj in self._inserts:
120 120 value.insert(idx, obj)
121 121 value[:0] = self._prepend
122 122 value.extend(self._extend)
123 123
124 124 elif isinstance(value, dict):
125 125 if self._update:
126 126 value.update(self._update)
127 127 elif isinstance(value, set):
128 128 if self._update:
129 129 value.update(self._update)
130 130 self._value = value
131 131 return value
132 132
133 133 def to_dict(self):
134 134 """return JSONable dict form of my data
135 135
136 136 Currently update as dict or set, extend, prepend as lists, and inserts as list of tuples.
137 137 """
138 138 d = {}
139 139 if self._update:
140 140 d['update'] = self._update
141 141 if self._extend:
142 142 d['extend'] = self._extend
143 143 if self._prepend:
144 144 d['prepend'] = self._prepend
145 145 elif self._inserts:
146 146 d['inserts'] = self._inserts
147 147 return d
148 148
149 149
150 150 def _is_section_key(key):
151 151 """Is a Config key a section name (does it start with a capital)?"""
152 152 if key and key[0].upper()==key[0] and not key.startswith('_'):
153 153 return True
154 154 else:
155 155 return False
156 156
157 157
158 158 class Config(dict):
159 159 """An attribute based dict that can do smart merges."""
160 160
161 161 def __init__(self, *args, **kwds):
162 162 dict.__init__(self, *args, **kwds)
163 163 self._ensure_subconfig()
164 164
165 165 def _ensure_subconfig(self):
166 166 """ensure that sub-dicts that should be Config objects are
167 167
168 168 casts dicts that are under section keys to Config objects,
169 169 which is necessary for constructing Config objects from dict literals.
170 170 """
171 171 for key in self:
172 172 obj = self[key]
173 173 if _is_section_key(key) \
174 174 and isinstance(obj, dict) \
175 175 and not isinstance(obj, Config):
176 176 setattr(self, key, Config(obj))
177 177
178 178 def _merge(self, other):
179 179 """deprecated alias, use Config.merge()"""
180 180 self.merge(other)
181 181
182 182 def merge(self, other):
183 183 """merge another config object into this one"""
184 184 to_update = {}
185 185 for k, v in iteritems(other):
186 186 if k not in self:
187 187 to_update[k] = copy.deepcopy(v)
188 188 else: # I have this key
189 189 if isinstance(v, Config) and isinstance(self[k], Config):
190 190 # Recursively merge common sub Configs
191 191 self[k].merge(v)
192 192 else:
193 193 # Plain updates for non-Configs
194 194 to_update[k] = copy.deepcopy(v)
195 195
196 196 self.update(to_update)
197 197
198 198 def collisions(self, other):
199 199 """Check for collisions between two config objects.
200 200
201 201 Returns a dict of the form {"Class": {"trait": "collision message"}}`,
202 202 indicating which values have been ignored.
203 203
204 204 An empty dict indicates no collisions.
205 205 """
206 206 collisions = {}
207 207 for section in self:
208 208 if section not in other:
209 209 continue
210 210 mine = self[section]
211 211 theirs = other[section]
212 212 for key in mine:
213 213 if key in theirs and mine[key] != theirs[key]:
214 214 collisions.setdefault(section, {})
215 215 collisions[section][key] = "%r ignored, using %r" % (mine[key], theirs[key])
216 216 return collisions
217 217
218 218 def __contains__(self, key):
219 219 # allow nested contains of the form `"Section.key" in config`
220 220 if '.' in key:
221 221 first, remainder = key.split('.', 1)
222 222 if first not in self:
223 223 return False
224 224 return remainder in self[first]
225 225
226 226 return super(Config, self).__contains__(key)
227 227
228 228 # .has_key is deprecated for dictionaries.
229 229 has_key = __contains__
230 230
231 231 def _has_section(self, key):
232 232 return _is_section_key(key) and key in self
233 233
234 234 def copy(self):
235 235 return type(self)(dict.copy(self))
236 236 # copy nested config objects
237 237 for k, v in self.items():
238 238 if isinstance(v, Config):
239 239 new_config[k] = v.copy()
240 240 return new_config
241 241
242 242 def __copy__(self):
243 243 return self.copy()
244 244
245 245 def __deepcopy__(self, memo):
246 246 new_config = type(self)()
247 247 for key, value in self.items():
248 248 if isinstance(value, (Config, LazyConfigValue)):
249 249 # deep copy config objects
250 250 value = copy.deepcopy(value, memo)
251 251 elif type(value) in {dict, list, set, tuple}:
252 252 # shallow copy plain container traits
253 253 value = copy.copy(value)
254 254 new_config[key] = value
255 255 return new_config
256 256
257 257 def __getitem__(self, key):
258 258 try:
259 259 return dict.__getitem__(self, key)
260 260 except KeyError:
261 261 if _is_section_key(key):
262 262 c = Config()
263 263 dict.__setitem__(self, key, c)
264 264 return c
265 265 elif not key.startswith('_'):
266 266 # undefined, create lazy value, used for container methods
267 267 v = LazyConfigValue()
268 268 dict.__setitem__(self, key, v)
269 269 return v
270 270 else:
271 271 raise KeyError
272 272
273 273 def __setitem__(self, key, value):
274 274 if _is_section_key(key):
275 275 if not isinstance(value, Config):
276 276 raise ValueError('values whose keys begin with an uppercase '
277 277 'char must be Config instances: %r, %r' % (key, value))
278 278 dict.__setitem__(self, key, value)
279 279
280 280 def __getattr__(self, key):
281 281 if key.startswith('__'):
282 282 return dict.__getattr__(self, key)
283 283 try:
284 284 return self.__getitem__(key)
285 285 except KeyError as e:
286 286 raise AttributeError(e)
287 287
288 288 def __setattr__(self, key, value):
289 289 if key.startswith('__'):
290 290 return dict.__setattr__(self, key, value)
291 291 try:
292 292 self.__setitem__(key, value)
293 293 except KeyError as e:
294 294 raise AttributeError(e)
295 295
296 296 def __delattr__(self, key):
297 297 if key.startswith('__'):
298 298 return dict.__delattr__(self, key)
299 299 try:
300 300 dict.__delitem__(self, key)
301 301 except KeyError as e:
302 302 raise AttributeError(e)
303 303
304 304
305 305 #-----------------------------------------------------------------------------
306 306 # Config loading classes
307 307 #-----------------------------------------------------------------------------
308 308
309 309
310 310 class ConfigLoader(object):
311 311 """A object for loading configurations from just about anywhere.
312 312
313 313 The resulting configuration is packaged as a :class:`Config`.
314 314
315 315 Notes
316 316 -----
317 317 A :class:`ConfigLoader` does one thing: load a config from a source
318 318 (file, command line arguments) and returns the data as a :class:`Config` object.
319 319 There are lots of things that :class:`ConfigLoader` does not do. It does
320 320 not implement complex logic for finding config files. It does not handle
321 321 default values or merge multiple configs. These things need to be
322 322 handled elsewhere.
323 323 """
324 324
325 325 def _log_default(self):
326 326 from IPython.utils.log import get_logger
327 327 return get_logger()
328 328
329 329 def __init__(self, log=None):
330 330 """A base class for config loaders.
331 331
332 332 log : instance of :class:`logging.Logger` to use.
333 By default loger of :meth:`IPython.config.application.Application.instance()`
333 By default loger of :meth:`traitlets.config.application.Application.instance()`
334 334 will be used
335 335
336 336 Examples
337 337 --------
338 338
339 339 >>> cl = ConfigLoader()
340 340 >>> config = cl.load_config()
341 341 >>> config
342 342 {}
343 343 """
344 344 self.clear()
345 345 if log is None:
346 346 self.log = self._log_default()
347 347 self.log.debug('Using default logger')
348 348 else:
349 349 self.log = log
350 350
351 351 def clear(self):
352 352 self.config = Config()
353 353
354 354 def load_config(self):
355 355 """Load a config from somewhere, return a :class:`Config` instance.
356 356
357 357 Usually, this will cause self.config to be set and then returned.
358 358 However, in most cases, :meth:`ConfigLoader.clear` should be called
359 359 to erase any previous state.
360 360 """
361 361 self.clear()
362 362 return self.config
363 363
364 364
365 365 class FileConfigLoader(ConfigLoader):
366 366 """A base class for file based configurations.
367 367
368 368 As we add more file based config loaders, the common logic should go
369 369 here.
370 370 """
371 371
372 372 def __init__(self, filename, path=None, **kw):
373 373 """Build a config loader for a filename and path.
374 374
375 375 Parameters
376 376 ----------
377 377 filename : str
378 378 The file name of the config file.
379 379 path : str, list, tuple
380 380 The path to search for the config file on, or a sequence of
381 381 paths to try in order.
382 382 """
383 383 super(FileConfigLoader, self).__init__(**kw)
384 384 self.filename = filename
385 385 self.path = path
386 386 self.full_filename = ''
387 387
388 388 def _find_file(self):
389 389 """Try to find the file by searching the paths."""
390 390 self.full_filename = filefind(self.filename, self.path)
391 391
392 392 class JSONFileConfigLoader(FileConfigLoader):
393 393 """A JSON file loader for config"""
394 394
395 395 def load_config(self):
396 396 """Load the config from a file and return it as a Config object."""
397 397 self.clear()
398 398 try:
399 399 self._find_file()
400 400 except IOError as e:
401 401 raise ConfigFileNotFound(str(e))
402 402 dct = self._read_file_as_dict()
403 403 self.config = self._convert_to_config(dct)
404 404 return self.config
405 405
406 406 def _read_file_as_dict(self):
407 407 with open(self.full_filename) as f:
408 408 return json.load(f)
409 409
410 410 def _convert_to_config(self, dictionary):
411 411 if 'version' in dictionary:
412 412 version = dictionary.pop('version')
413 413 else:
414 414 version = 1
415 415 self.log.warn("Unrecognized JSON config file version, assuming version {}".format(version))
416 416
417 417 if version == 1:
418 418 return Config(dictionary)
419 419 else:
420 420 raise ValueError('Unknown version of JSON config file: {version}'.format(version=version))
421 421
422 422
423 423 class PyFileConfigLoader(FileConfigLoader):
424 424 """A config loader for pure python files.
425 425
426 426 This is responsible for locating a Python config file by filename and
427 427 path, then executing it to construct a Config object.
428 428 """
429 429
430 430 def load_config(self):
431 431 """Load the config from a file and return it as a Config object."""
432 432 self.clear()
433 433 try:
434 434 self._find_file()
435 435 except IOError as e:
436 436 raise ConfigFileNotFound(str(e))
437 437 self._read_file_as_dict()
438 438 return self.config
439 439
440 440 def load_subconfig(self, fname, path=None):
441 441 """Injected into config file namespace as load_subconfig"""
442 442 if path is None:
443 443 path = self.path
444 444
445 445 loader = self.__class__(fname, path)
446 446 try:
447 447 sub_config = loader.load_config()
448 448 except ConfigFileNotFound:
449 449 # Pass silently if the sub config is not there,
450 450 # treat it as an empty config file.
451 451 pass
452 452 else:
453 453 self.config.merge(sub_config)
454 454
455 455 def _read_file_as_dict(self):
456 456 """Load the config file into self.config, with recursive loading."""
457 457 def get_config():
458 458 """Unnecessary now, but a deprecation warning is more trouble than it's worth."""
459 459 return self.config
460 460
461 461 namespace = dict(
462 462 c=self.config,
463 463 load_subconfig=self.load_subconfig,
464 464 get_config=get_config,
465 465 __file__=self.full_filename,
466 466 )
467 467 fs_encoding = sys.getfilesystemencoding() or 'ascii'
468 468 conf_filename = self.full_filename.encode(fs_encoding)
469 469 py3compat.execfile(conf_filename, namespace)
470 470
471 471
472 472 class CommandLineConfigLoader(ConfigLoader):
473 473 """A config loader for command line arguments.
474 474
475 475 As we add more command line based loaders, the common logic should go
476 476 here.
477 477 """
478 478
479 479 def _exec_config_str(self, lhs, rhs):
480 480 """execute self.config.<lhs> = <rhs>
481 481
482 482 * expands ~ with expanduser
483 483 * tries to assign with literal_eval, otherwise assigns with just the string,
484 484 allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not*
485 485 equivalent are `--C.a=4` and `--C.a='4'`.
486 486 """
487 487 rhs = os.path.expanduser(rhs)
488 488 try:
489 489 # Try to see if regular Python syntax will work. This
490 490 # won't handle strings as the quote marks are removed
491 491 # by the system shell.
492 492 value = literal_eval(rhs)
493 493 except (NameError, SyntaxError, ValueError):
494 494 # This case happens if the rhs is a string.
495 495 value = rhs
496 496
497 497 exec(u'self.config.%s = value' % lhs)
498 498
499 499 def _load_flag(self, cfg):
500 500 """update self.config from a flag, which can be a dict or Config"""
501 501 if isinstance(cfg, (dict, Config)):
502 502 # don't clobber whole config sections, update
503 503 # each section from config:
504 504 for sec,c in iteritems(cfg):
505 505 self.config[sec].update(c)
506 506 else:
507 507 raise TypeError("Invalid flag: %r" % cfg)
508 508
509 509 # raw --identifier=value pattern
510 510 # but *also* accept '-' as wordsep, for aliases
511 511 # accepts: --foo=a
512 512 # --Class.trait=value
513 513 # --alias-name=value
514 514 # rejects: -foo=value
515 515 # --foo
516 516 # --Class.trait
517 517 kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*')
518 518
519 519 # just flags, no assignments, with two *or one* leading '-'
520 520 # accepts: --foo
521 521 # -foo-bar-again
522 522 # rejects: --anything=anything
523 523 # --two.word
524 524
525 525 flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$')
526 526
527 527 class KeyValueConfigLoader(CommandLineConfigLoader):
528 528 """A config loader that loads key value pairs from the command line.
529 529
530 530 This allows command line options to be gives in the following form::
531 531
532 532 ipython --profile="foo" --InteractiveShell.autocall=False
533 533 """
534 534
535 535 def __init__(self, argv=None, aliases=None, flags=None, **kw):
536 536 """Create a key value pair config loader.
537 537
538 538 Parameters
539 539 ----------
540 540 argv : list
541 541 A list that has the form of sys.argv[1:] which has unicode
542 542 elements of the form u"key=value". If this is None (default),
543 543 then sys.argv[1:] will be used.
544 544 aliases : dict
545 545 A dict of aliases for configurable traits.
546 546 Keys are the short aliases, Values are the resolved trait.
547 547 Of the form: `{'alias' : 'Configurable.trait'}`
548 548 flags : dict
549 549 A dict of flags, keyed by str name. Vaues can be Config objects,
550 550 dicts, or "key=value" strings. If Config or dict, when the flag
551 551 is triggered, The flag is loaded as `self.config.update(m)`.
552 552
553 553 Returns
554 554 -------
555 555 config : Config
556 556 The resulting Config object.
557 557
558 558 Examples
559 559 --------
560 560
561 >>> from IPython.config.loader import KeyValueConfigLoader
561 >>> from traitlets.config.loader import KeyValueConfigLoader
562 562 >>> cl = KeyValueConfigLoader()
563 563 >>> d = cl.load_config(["--A.name='brian'","--B.number=0"])
564 564 >>> sorted(d.items())
565 565 [('A', {'name': 'brian'}), ('B', {'number': 0})]
566 566 """
567 567 super(KeyValueConfigLoader, self).__init__(**kw)
568 568 if argv is None:
569 569 argv = sys.argv[1:]
570 570 self.argv = argv
571 571 self.aliases = aliases or {}
572 572 self.flags = flags or {}
573 573
574 574
575 575 def clear(self):
576 576 super(KeyValueConfigLoader, self).clear()
577 577 self.extra_args = []
578 578
579 579
580 580 def _decode_argv(self, argv, enc=None):
581 581 """decode argv if bytes, using stdin.encoding, falling back on default enc"""
582 582 uargv = []
583 583 if enc is None:
584 584 enc = DEFAULT_ENCODING
585 585 for arg in argv:
586 586 if not isinstance(arg, unicode_type):
587 587 # only decode if not already decoded
588 588 arg = arg.decode(enc)
589 589 uargv.append(arg)
590 590 return uargv
591 591
592 592
593 593 def load_config(self, argv=None, aliases=None, flags=None):
594 594 """Parse the configuration and generate the Config object.
595 595
596 596 After loading, any arguments that are not key-value or
597 597 flags will be stored in self.extra_args - a list of
598 598 unparsed command-line arguments. This is used for
599 599 arguments such as input files or subcommands.
600 600
601 601 Parameters
602 602 ----------
603 603 argv : list, optional
604 604 A list that has the form of sys.argv[1:] which has unicode
605 605 elements of the form u"key=value". If this is None (default),
606 606 then self.argv will be used.
607 607 aliases : dict
608 608 A dict of aliases for configurable traits.
609 609 Keys are the short aliases, Values are the resolved trait.
610 610 Of the form: `{'alias' : 'Configurable.trait'}`
611 611 flags : dict
612 612 A dict of flags, keyed by str name. Values can be Config objects
613 613 or dicts. When the flag is triggered, The config is loaded as
614 614 `self.config.update(cfg)`.
615 615 """
616 616 self.clear()
617 617 if argv is None:
618 618 argv = self.argv
619 619 if aliases is None:
620 620 aliases = self.aliases
621 621 if flags is None:
622 622 flags = self.flags
623 623
624 624 # ensure argv is a list of unicode strings:
625 625 uargv = self._decode_argv(argv)
626 626 for idx,raw in enumerate(uargv):
627 627 # strip leading '-'
628 628 item = raw.lstrip('-')
629 629
630 630 if raw == '--':
631 631 # don't parse arguments after '--'
632 632 # this is useful for relaying arguments to scripts, e.g.
633 633 # ipython -i foo.py --matplotlib=qt -- args after '--' go-to-foo.py
634 634 self.extra_args.extend(uargv[idx+1:])
635 635 break
636 636
637 637 if kv_pattern.match(raw):
638 638 lhs,rhs = item.split('=',1)
639 639 # Substitute longnames for aliases.
640 640 if lhs in aliases:
641 641 lhs = aliases[lhs]
642 642 if '.' not in lhs:
643 643 # probably a mistyped alias, but not technically illegal
644 644 self.log.warn("Unrecognized alias: '%s', it will probably have no effect.", raw)
645 645 try:
646 646 self._exec_config_str(lhs, rhs)
647 647 except Exception:
648 648 raise ArgumentError("Invalid argument: '%s'" % raw)
649 649
650 650 elif flag_pattern.match(raw):
651 651 if item in flags:
652 652 cfg,help = flags[item]
653 653 self._load_flag(cfg)
654 654 else:
655 655 raise ArgumentError("Unrecognized flag: '%s'"%raw)
656 656 elif raw.startswith('-'):
657 657 kv = '--'+item
658 658 if kv_pattern.match(kv):
659 659 raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv))
660 660 else:
661 661 raise ArgumentError("Invalid argument: '%s'"%raw)
662 662 else:
663 663 # keep all args that aren't valid in a list,
664 664 # in case our parent knows what to do with them.
665 665 self.extra_args.append(item)
666 666 return self.config
667 667
668 668 class ArgParseConfigLoader(CommandLineConfigLoader):
669 669 """A loader that uses the argparse module to load from the command line."""
670 670
671 671 def __init__(self, argv=None, aliases=None, flags=None, log=None, *parser_args, **parser_kw):
672 672 """Create a config loader for use with argparse.
673 673
674 674 Parameters
675 675 ----------
676 676
677 677 argv : optional, list
678 678 If given, used to read command-line arguments from, otherwise
679 679 sys.argv[1:] is used.
680 680
681 681 parser_args : tuple
682 682 A tuple of positional arguments that will be passed to the
683 683 constructor of :class:`argparse.ArgumentParser`.
684 684
685 685 parser_kw : dict
686 686 A tuple of keyword arguments that will be passed to the
687 687 constructor of :class:`argparse.ArgumentParser`.
688 688
689 689 Returns
690 690 -------
691 691 config : Config
692 692 The resulting Config object.
693 693 """
694 694 super(CommandLineConfigLoader, self).__init__(log=log)
695 695 self.clear()
696 696 if argv is None:
697 697 argv = sys.argv[1:]
698 698 self.argv = argv
699 699 self.aliases = aliases or {}
700 700 self.flags = flags or {}
701 701
702 702 self.parser_args = parser_args
703 703 self.version = parser_kw.pop("version", None)
704 704 kwargs = dict(argument_default=argparse.SUPPRESS)
705 705 kwargs.update(parser_kw)
706 706 self.parser_kw = kwargs
707 707
708 708 def load_config(self, argv=None, aliases=None, flags=None):
709 709 """Parse command line arguments and return as a Config object.
710 710
711 711 Parameters
712 712 ----------
713 713
714 714 args : optional, list
715 715 If given, a list with the structure of sys.argv[1:] to parse
716 716 arguments from. If not given, the instance's self.argv attribute
717 717 (given at construction time) is used."""
718 718 self.clear()
719 719 if argv is None:
720 720 argv = self.argv
721 721 if aliases is None:
722 722 aliases = self.aliases
723 723 if flags is None:
724 724 flags = self.flags
725 725 self._create_parser(aliases, flags)
726 726 self._parse_args(argv)
727 727 self._convert_to_config()
728 728 return self.config
729 729
730 730 def get_extra_args(self):
731 731 if hasattr(self, 'extra_args'):
732 732 return self.extra_args
733 733 else:
734 734 return []
735 735
736 736 def _create_parser(self, aliases=None, flags=None):
737 737 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
738 738 self._add_arguments(aliases, flags)
739 739
740 740 def _add_arguments(self, aliases=None, flags=None):
741 741 raise NotImplementedError("subclasses must implement _add_arguments")
742 742
743 743 def _parse_args(self, args):
744 744 """self.parser->self.parsed_data"""
745 745 # decode sys.argv to support unicode command-line options
746 746 enc = DEFAULT_ENCODING
747 747 uargs = [py3compat.cast_unicode(a, enc) for a in args]
748 748 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
749 749
750 750 def _convert_to_config(self):
751 751 """self.parsed_data->self.config"""
752 752 for k, v in iteritems(vars(self.parsed_data)):
753 753 exec("self.config.%s = v"%k, locals(), globals())
754 754
755 755 class KVArgParseConfigLoader(ArgParseConfigLoader):
756 756 """A config loader that loads aliases and flags with argparse,
757 757 but will use KVLoader for the rest. This allows better parsing
758 758 of common args, such as `ipython -c 'print 5'`, but still gets
759 759 arbitrary config with `ipython --InteractiveShell.use_readline=False`"""
760 760
761 761 def _add_arguments(self, aliases=None, flags=None):
762 762 self.alias_flags = {}
763 763 # print aliases, flags
764 764 if aliases is None:
765 765 aliases = self.aliases
766 766 if flags is None:
767 767 flags = self.flags
768 768 paa = self.parser.add_argument
769 769 for key,value in iteritems(aliases):
770 770 if key in flags:
771 771 # flags
772 772 nargs = '?'
773 773 else:
774 774 nargs = None
775 775 if len(key) is 1:
776 776 paa('-'+key, '--'+key, type=unicode_type, dest=value, nargs=nargs)
777 777 else:
778 778 paa('--'+key, type=unicode_type, dest=value, nargs=nargs)
779 779 for key, (value, help) in iteritems(flags):
780 780 if key in self.aliases:
781 781 #
782 782 self.alias_flags[self.aliases[key]] = value
783 783 continue
784 784 if len(key) is 1:
785 785 paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
786 786 else:
787 787 paa('--'+key, action='append_const', dest='_flags', const=value)
788 788
789 789 def _convert_to_config(self):
790 790 """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""
791 791 # remove subconfigs list from namespace before transforming the Namespace
792 792 if '_flags' in self.parsed_data:
793 793 subcs = self.parsed_data._flags
794 794 del self.parsed_data._flags
795 795 else:
796 796 subcs = []
797 797
798 798 for k, v in iteritems(vars(self.parsed_data)):
799 799 if v is None:
800 800 # it was a flag that shares the name of an alias
801 801 subcs.append(self.alias_flags[k])
802 802 else:
803 803 # eval the KV assignment
804 804 self._exec_config_str(k, v)
805 805
806 806 for subc in subcs:
807 807 self._load_flag(subc)
808 808
809 809 if self.extra_args:
810 810 sub_parser = KeyValueConfigLoader(log=self.log)
811 811 sub_parser.load_config(self.extra_args)
812 812 self.config.merge(sub_parser.config)
813 813 self.extra_args = sub_parser.extra_args
814 814
815 815
816 816 def load_pyconfig_files(config_files, path):
817 817 """Load multiple Python config files, merging each of them in turn.
818 818
819 819 Parameters
820 820 ==========
821 821 config_files : list of str
822 822 List of config files names to load and merge into the config.
823 823 path : unicode
824 824 The full path to the location of the config files.
825 825 """
826 826 config = Config()
827 827 for cf in config_files:
828 828 loader = PyFileConfigLoader(cf, path=path)
829 829 try:
830 830 next_config = loader.load_config()
831 831 except ConfigFileNotFound:
832 832 pass
833 833 except:
834 834 raise
835 835 else:
836 836 config.merge(next_config)
837 837 return config
@@ -1,88 +1,88 b''
1 1 """Manager to read and modify config data in JSON files.
2 2 """
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5 import errno
6 6 import io
7 7 import json
8 8 import os
9 9
10 from IPython.config import LoggingConfigurable
10 from traitlets.config import LoggingConfigurable
11 11 from IPython.utils.py3compat import PY3
12 from IPython.utils.traitlets import Unicode
12 from traitlets.traitlets import Unicode
13 13
14 14
15 15 def recursive_update(target, new):
16 16 """Recursively update one dictionary using another.
17 17
18 18 None values will delete their keys.
19 19 """
20 20 for k, v in new.items():
21 21 if isinstance(v, dict):
22 22 if k not in target:
23 23 target[k] = {}
24 24 recursive_update(target[k], v)
25 25 if not target[k]:
26 26 # Prune empty subdicts
27 27 del target[k]
28 28
29 29 elif v is None:
30 30 target.pop(k, None)
31 31
32 32 else:
33 33 target[k] = v
34 34
35 35
36 36 class BaseJSONConfigManager(LoggingConfigurable):
37 37 """General JSON config manager
38 38
39 39 Deals with persisting/storing config in a json file
40 40 """
41 41
42 42 config_dir = Unicode('.')
43 43
44 44 def ensure_config_dir_exists(self):
45 45 try:
46 46 os.mkdir(self.config_dir, 0o755)
47 47 except OSError as e:
48 48 if e.errno != errno.EEXIST:
49 49 raise
50 50
51 51 def file_name(self, section_name):
52 52 return os.path.join(self.config_dir, section_name+'.json')
53 53
54 54 def get(self, section_name):
55 55 """Retrieve the config data for the specified section.
56 56
57 57 Returns the data as a dictionary, or an empty dictionary if the file
58 58 doesn't exist.
59 59 """
60 60 filename = self.file_name(section_name)
61 61 if os.path.isfile(filename):
62 62 with io.open(filename, encoding='utf-8') as f:
63 63 return json.load(f)
64 64 else:
65 65 return {}
66 66
67 67 def set(self, section_name, data):
68 68 """Store the given config data.
69 69 """
70 70 filename = self.file_name(section_name)
71 71 self.ensure_config_dir_exists()
72 72
73 73 if PY3:
74 74 f = io.open(filename, 'w', encoding='utf-8')
75 75 else:
76 76 f = open(filename, 'wb')
77 77 with f:
78 78 json.dump(data, f, indent=2)
79 79
80 80 def update(self, section_name, new_data):
81 81 """Modify the config section by recursively updating it with new_data.
82 82
83 83 Returns the modified config data as a dictionary.
84 84 """
85 85 data = self.get(section_name)
86 86 recursive_update(data, new_data)
87 87 self.set(section_name, data)
88 88 return data
1 NO CONTENT: file renamed from IPython/config/tests/__init__.py to traitlets/config/tests/__init__.py
@@ -1,199 +1,199 b''
1 1 # coding: utf-8
2 2 """
3 Tests for IPython.config.application.Application
3 Tests for traitlets.config.application.Application
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 import logging
10 10 import os
11 11 from io import StringIO
12 12 from unittest import TestCase
13 13
14 14 pjoin = os.path.join
15 15
16 16 import nose.tools as nt
17 17
18 from IPython.config.configurable import Configurable
19 from IPython.config.loader import Config
18 from traitlets.config.configurable import Configurable
19 from traitlets.config.loader import Config
20 20
21 from IPython.config.application import (
21 from traitlets.config.application import (
22 22 Application
23 23 )
24 24
25 25 from IPython.utils.tempdir import TemporaryDirectory
26 from IPython.utils.traitlets import (
26 from traitlets.traitlets import (
27 27 Bool, Unicode, Integer, List, Dict
28 28 )
29 29
30 30
31 31 class Foo(Configurable):
32 32
33 33 i = Integer(0, config=True, help="The integer i.")
34 34 j = Integer(1, config=True, help="The integer j.")
35 35 name = Unicode(u'Brian', config=True, help="First name.")
36 36
37 37
38 38 class Bar(Configurable):
39 39
40 40 b = Integer(0, config=True, help="The integer b.")
41 41 enabled = Bool(True, config=True, help="Enable bar.")
42 42
43 43
44 44 class MyApp(Application):
45 45
46 46 name = Unicode(u'myapp')
47 47 running = Bool(False, config=True,
48 48 help="Is the app running?")
49 49 classes = List([Bar, Foo])
50 50 config_file = Unicode(u'', config=True,
51 51 help="Load this config file")
52 52
53 53 aliases = Dict({
54 54 'i' : 'Foo.i',
55 55 'j' : 'Foo.j',
56 56 'name' : 'Foo.name',
57 57 'enabled' : 'Bar.enabled',
58 58 'log-level' : 'Application.log_level',
59 59 })
60 60
61 61 flags = Dict(dict(enable=({'Bar': {'enabled' : True}}, "Set Bar.enabled to True"),
62 62 disable=({'Bar': {'enabled' : False}}, "Set Bar.enabled to False"),
63 63 crit=({'Application' : {'log_level' : logging.CRITICAL}},
64 64 "set level=CRITICAL"),
65 65 ))
66 66
67 67 def init_foo(self):
68 68 self.foo = Foo(parent=self)
69 69
70 70 def init_bar(self):
71 71 self.bar = Bar(parent=self)
72 72
73 73
74 74 class TestApplication(TestCase):
75 75
76 76 def test_log(self):
77 77 stream = StringIO()
78 78 app = MyApp(log_level=logging.INFO)
79 79 handler = logging.StreamHandler(stream)
80 80 # trigger reconstruction of the log formatter
81 81 app.log.handlers = [handler]
82 82 app.log_format = "%(message)s"
83 83 app.log_datefmt = "%Y-%m-%d %H:%M"
84 84 app.log.info("hello")
85 85 nt.assert_in("hello", stream.getvalue())
86 86
87 87 def test_basic(self):
88 88 app = MyApp()
89 89 self.assertEqual(app.name, u'myapp')
90 90 self.assertEqual(app.running, False)
91 91 self.assertEqual(app.classes, [MyApp,Bar,Foo])
92 92 self.assertEqual(app.config_file, u'')
93 93
94 94 def test_config(self):
95 95 app = MyApp()
96 96 app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
97 97 config = app.config
98 98 self.assertEqual(config.Foo.i, 10)
99 99 self.assertEqual(config.Foo.j, 10)
100 100 self.assertEqual(config.Bar.enabled, False)
101 101 self.assertEqual(config.MyApp.log_level,50)
102 102
103 103 def test_config_propagation(self):
104 104 app = MyApp()
105 105 app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
106 106 app.init_foo()
107 107 app.init_bar()
108 108 self.assertEqual(app.foo.i, 10)
109 109 self.assertEqual(app.foo.j, 10)
110 110 self.assertEqual(app.bar.enabled, False)
111 111
112 112 def test_flags(self):
113 113 app = MyApp()
114 114 app.parse_command_line(["--disable"])
115 115 app.init_bar()
116 116 self.assertEqual(app.bar.enabled, False)
117 117 app.parse_command_line(["--enable"])
118 118 app.init_bar()
119 119 self.assertEqual(app.bar.enabled, True)
120 120
121 121 def test_aliases(self):
122 122 app = MyApp()
123 123 app.parse_command_line(["--i=5", "--j=10"])
124 124 app.init_foo()
125 125 self.assertEqual(app.foo.i, 5)
126 126 app.init_foo()
127 127 self.assertEqual(app.foo.j, 10)
128 128
129 129 def test_flag_clobber(self):
130 130 """test that setting flags doesn't clobber existing settings"""
131 131 app = MyApp()
132 132 app.parse_command_line(["--Bar.b=5", "--disable"])
133 133 app.init_bar()
134 134 self.assertEqual(app.bar.enabled, False)
135 135 self.assertEqual(app.bar.b, 5)
136 136 app.parse_command_line(["--enable", "--Bar.b=10"])
137 137 app.init_bar()
138 138 self.assertEqual(app.bar.enabled, True)
139 139 self.assertEqual(app.bar.b, 10)
140 140
141 141 def test_flatten_flags(self):
142 142 cfg = Config()
143 143 cfg.MyApp.log_level = logging.WARN
144 144 app = MyApp()
145 145 app.update_config(cfg)
146 146 self.assertEqual(app.log_level, logging.WARN)
147 147 self.assertEqual(app.config.MyApp.log_level, logging.WARN)
148 148 app.initialize(["--crit"])
149 149 self.assertEqual(app.log_level, logging.CRITICAL)
150 150 # this would be app.config.Application.log_level if it failed:
151 151 self.assertEqual(app.config.MyApp.log_level, logging.CRITICAL)
152 152
153 153 def test_flatten_aliases(self):
154 154 cfg = Config()
155 155 cfg.MyApp.log_level = logging.WARN
156 156 app = MyApp()
157 157 app.update_config(cfg)
158 158 self.assertEqual(app.log_level, logging.WARN)
159 159 self.assertEqual(app.config.MyApp.log_level, logging.WARN)
160 160 app.initialize(["--log-level", "CRITICAL"])
161 161 self.assertEqual(app.log_level, logging.CRITICAL)
162 162 # this would be app.config.Application.log_level if it failed:
163 163 self.assertEqual(app.config.MyApp.log_level, "CRITICAL")
164 164
165 165 def test_extra_args(self):
166 166 app = MyApp()
167 167 app.parse_command_line(["--Bar.b=5", 'extra', "--disable", 'args'])
168 168 app.init_bar()
169 169 self.assertEqual(app.bar.enabled, False)
170 170 self.assertEqual(app.bar.b, 5)
171 171 self.assertEqual(app.extra_args, ['extra', 'args'])
172 172 app = MyApp()
173 173 app.parse_command_line(["--Bar.b=5", '--', 'extra', "--disable", 'args'])
174 174 app.init_bar()
175 175 self.assertEqual(app.bar.enabled, True)
176 176 self.assertEqual(app.bar.b, 5)
177 177 self.assertEqual(app.extra_args, ['extra', '--disable', 'args'])
178 178
179 179 def test_unicode_argv(self):
180 180 app = MyApp()
181 181 app.parse_command_line(['ünîcødé'])
182 182
183 183 def test_multi_file(self):
184 184 app = MyApp()
185 185 app.log = logging.getLogger()
186 186 name = 'config.py'
187 187 with TemporaryDirectory('_1') as td1:
188 188 with open(pjoin(td1, name), 'w') as f1:
189 189 f1.write("get_config().MyApp.Bar.b = 1")
190 190 with TemporaryDirectory('_2') as td2:
191 191 with open(pjoin(td2, name), 'w') as f2:
192 192 f2.write("get_config().MyApp.Bar.b = 2")
193 193 app.load_config_file(name, path=[td2, td1])
194 194 app.init_bar()
195 195 self.assertEqual(app.bar.b, 2)
196 196 app.load_config_file(name, path=[td1, td2])
197 197 app.init_bar()
198 198 self.assertEqual(app.bar.b, 1)
199 199
@@ -1,378 +1,378 b''
1 1 # encoding: utf-8
2 """Tests for IPython.config.configurable"""
2 """Tests for traitlets.config.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 unittest import TestCase
8 8
9 from IPython.config.configurable import (
9 from traitlets.config.configurable import (
10 10 Configurable,
11 11 SingletonConfigurable
12 12 )
13 13
14 from IPython.utils.traitlets import (
14 from traitlets.traitlets import (
15 15 Integer, Float, Unicode, List, Dict, Set,
16 16 )
17 17
18 from IPython.config.loader import Config
18 from traitlets.config.loader import Config
19 19 from IPython.utils.py3compat import PY3
20 20
21 21
22 22 class MyConfigurable(Configurable):
23 23 a = Integer(1, config=True, help="The integer a.")
24 24 b = Float(1.0, config=True, help="The integer b.")
25 25 c = Unicode('no config')
26 26
27 27
28 28 mc_help=u"""MyConfigurable options
29 29 ----------------------
30 30 --MyConfigurable.a=<Integer>
31 31 Default: 1
32 32 The integer a.
33 33 --MyConfigurable.b=<Float>
34 34 Default: 1.0
35 35 The integer b."""
36 36
37 37 mc_help_inst=u"""MyConfigurable options
38 38 ----------------------
39 39 --MyConfigurable.a=<Integer>
40 40 Current: 5
41 41 The integer a.
42 42 --MyConfigurable.b=<Float>
43 43 Current: 4.0
44 44 The integer b."""
45 45
46 46 # On Python 3, the Integer trait is a synonym for Int
47 47 if PY3:
48 48 mc_help = mc_help.replace(u"<Integer>", u"<Int>")
49 49 mc_help_inst = mc_help_inst.replace(u"<Integer>", u"<Int>")
50 50
51 51 class Foo(Configurable):
52 52 a = Integer(0, config=True, help="The integer a.")
53 53 b = Unicode('nope', config=True)
54 54
55 55
56 56 class Bar(Foo):
57 57 b = Unicode('gotit', config=False, help="The string b.")
58 58 c = Float(config=True, help="The string c.")
59 59
60 60
61 61 class TestConfigurable(TestCase):
62 62
63 63 def test_default(self):
64 64 c1 = Configurable()
65 65 c2 = Configurable(config=c1.config)
66 66 c3 = Configurable(config=c2.config)
67 67 self.assertEqual(c1.config, c2.config)
68 68 self.assertEqual(c2.config, c3.config)
69 69
70 70 def test_custom(self):
71 71 config = Config()
72 72 config.foo = 'foo'
73 73 config.bar = 'bar'
74 74 c1 = Configurable(config=config)
75 75 c2 = Configurable(config=c1.config)
76 76 c3 = Configurable(config=c2.config)
77 77 self.assertEqual(c1.config, config)
78 78 self.assertEqual(c2.config, config)
79 79 self.assertEqual(c3.config, config)
80 80 # Test that copies are not made
81 81 self.assertTrue(c1.config is config)
82 82 self.assertTrue(c2.config is config)
83 83 self.assertTrue(c3.config is config)
84 84 self.assertTrue(c1.config is c2.config)
85 85 self.assertTrue(c2.config is c3.config)
86 86
87 87 def test_inheritance(self):
88 88 config = Config()
89 89 config.MyConfigurable.a = 2
90 90 config.MyConfigurable.b = 2.0
91 91 c1 = MyConfigurable(config=config)
92 92 c2 = MyConfigurable(config=c1.config)
93 93 self.assertEqual(c1.a, config.MyConfigurable.a)
94 94 self.assertEqual(c1.b, config.MyConfigurable.b)
95 95 self.assertEqual(c2.a, config.MyConfigurable.a)
96 96 self.assertEqual(c2.b, config.MyConfigurable.b)
97 97
98 98 def test_parent(self):
99 99 config = Config()
100 100 config.Foo.a = 10
101 101 config.Foo.b = "wow"
102 102 config.Bar.b = 'later'
103 103 config.Bar.c = 100.0
104 104 f = Foo(config=config)
105 105 b = Bar(config=f.config)
106 106 self.assertEqual(f.a, 10)
107 107 self.assertEqual(f.b, 'wow')
108 108 self.assertEqual(b.b, 'gotit')
109 109 self.assertEqual(b.c, 100.0)
110 110
111 111 def test_override1(self):
112 112 config = Config()
113 113 config.MyConfigurable.a = 2
114 114 config.MyConfigurable.b = 2.0
115 115 c = MyConfigurable(a=3, config=config)
116 116 self.assertEqual(c.a, 3)
117 117 self.assertEqual(c.b, config.MyConfigurable.b)
118 118 self.assertEqual(c.c, 'no config')
119 119
120 120 def test_override2(self):
121 121 config = Config()
122 122 config.Foo.a = 1
123 123 config.Bar.b = 'or' # Up above b is config=False, so this won't do it.
124 124 config.Bar.c = 10.0
125 125 c = Bar(config=config)
126 126 self.assertEqual(c.a, config.Foo.a)
127 127 self.assertEqual(c.b, 'gotit')
128 128 self.assertEqual(c.c, config.Bar.c)
129 129 c = Bar(a=2, b='and', c=20.0, config=config)
130 130 self.assertEqual(c.a, 2)
131 131 self.assertEqual(c.b, 'and')
132 132 self.assertEqual(c.c, 20.0)
133 133
134 134 def test_help(self):
135 135 self.assertEqual(MyConfigurable.class_get_help(), mc_help)
136 136
137 137 def test_help_inst(self):
138 138 inst = MyConfigurable(a=5, b=4)
139 139 self.assertEqual(MyConfigurable.class_get_help(inst), mc_help_inst)
140 140
141 141
142 142 class TestSingletonConfigurable(TestCase):
143 143
144 144 def test_instance(self):
145 145 class Foo(SingletonConfigurable): pass
146 146 self.assertEqual(Foo.initialized(), False)
147 147 foo = Foo.instance()
148 148 self.assertEqual(Foo.initialized(), True)
149 149 self.assertEqual(foo, Foo.instance())
150 150 self.assertEqual(SingletonConfigurable._instance, None)
151 151
152 152 def test_inheritance(self):
153 153 class Bar(SingletonConfigurable): pass
154 154 class Bam(Bar): pass
155 155 self.assertEqual(Bar.initialized(), False)
156 156 self.assertEqual(Bam.initialized(), False)
157 157 bam = Bam.instance()
158 158 bam == Bar.instance()
159 159 self.assertEqual(Bar.initialized(), True)
160 160 self.assertEqual(Bam.initialized(), True)
161 161 self.assertEqual(bam, Bam._instance)
162 162 self.assertEqual(bam, Bar._instance)
163 163 self.assertEqual(SingletonConfigurable._instance, None)
164 164
165 165
166 166 class MyParent(Configurable):
167 167 pass
168 168
169 169 class MyParent2(MyParent):
170 170 pass
171 171
172 172 class TestParentConfigurable(TestCase):
173 173
174 174 def test_parent_config(self):
175 175 cfg = Config({
176 176 'MyParent' : {
177 177 'MyConfigurable' : {
178 178 'b' : 2.0,
179 179 }
180 180 }
181 181 })
182 182 parent = MyParent(config=cfg)
183 183 myc = MyConfigurable(parent=parent)
184 184 self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)
185 185
186 186 def test_parent_inheritance(self):
187 187 cfg = Config({
188 188 'MyParent' : {
189 189 'MyConfigurable' : {
190 190 'b' : 2.0,
191 191 }
192 192 }
193 193 })
194 194 parent = MyParent2(config=cfg)
195 195 myc = MyConfigurable(parent=parent)
196 196 self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)
197 197
198 198 def test_multi_parent(self):
199 199 cfg = Config({
200 200 'MyParent2' : {
201 201 'MyParent' : {
202 202 'MyConfigurable' : {
203 203 'b' : 2.0,
204 204 }
205 205 },
206 206 # this one shouldn't count
207 207 'MyConfigurable' : {
208 208 'b' : 3.0,
209 209 },
210 210 }
211 211 })
212 212 parent2 = MyParent2(config=cfg)
213 213 parent = MyParent(parent=parent2)
214 214 myc = MyConfigurable(parent=parent)
215 215 self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b)
216 216
217 217 def test_parent_priority(self):
218 218 cfg = Config({
219 219 'MyConfigurable' : {
220 220 'b' : 2.0,
221 221 },
222 222 'MyParent' : {
223 223 'MyConfigurable' : {
224 224 'b' : 3.0,
225 225 }
226 226 },
227 227 'MyParent2' : {
228 228 'MyConfigurable' : {
229 229 'b' : 4.0,
230 230 }
231 231 }
232 232 })
233 233 parent = MyParent2(config=cfg)
234 234 myc = MyConfigurable(parent=parent)
235 235 self.assertEqual(myc.b, parent.config.MyParent2.MyConfigurable.b)
236 236
237 237 def test_multi_parent_priority(self):
238 238 cfg = Config({
239 239 'MyConfigurable' : {
240 240 'b' : 2.0,
241 241 },
242 242 'MyParent' : {
243 243 'MyConfigurable' : {
244 244 'b' : 3.0,
245 245 }
246 246 },
247 247 'MyParent2' : {
248 248 'MyConfigurable' : {
249 249 'b' : 4.0,
250 250 }
251 251 },
252 252 'MyParent2' : {
253 253 'MyParent' : {
254 254 'MyConfigurable' : {
255 255 'b' : 5.0,
256 256 }
257 257 }
258 258 }
259 259 })
260 260 parent2 = MyParent2(config=cfg)
261 261 parent = MyParent2(parent=parent2)
262 262 myc = MyConfigurable(parent=parent)
263 263 self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b)
264 264
265 265 class Containers(Configurable):
266 266 lis = List(config=True)
267 267 def _lis_default(self):
268 268 return [-1]
269 269
270 270 s = Set(config=True)
271 271 def _s_default(self):
272 272 return {'a'}
273 273
274 274 d = Dict(config=True)
275 275 def _d_default(self):
276 276 return {'a' : 'b'}
277 277
278 278 class TestConfigContainers(TestCase):
279 279 def test_extend(self):
280 280 c = Config()
281 281 c.Containers.lis.extend(list(range(5)))
282 282 obj = Containers(config=c)
283 283 self.assertEqual(obj.lis, list(range(-1,5)))
284 284
285 285 def test_insert(self):
286 286 c = Config()
287 287 c.Containers.lis.insert(0, 'a')
288 288 c.Containers.lis.insert(1, 'b')
289 289 obj = Containers(config=c)
290 290 self.assertEqual(obj.lis, ['a', 'b', -1])
291 291
292 292 def test_prepend(self):
293 293 c = Config()
294 294 c.Containers.lis.prepend([1,2])
295 295 c.Containers.lis.prepend([2,3])
296 296 obj = Containers(config=c)
297 297 self.assertEqual(obj.lis, [2,3,1,2,-1])
298 298
299 299 def test_prepend_extend(self):
300 300 c = Config()
301 301 c.Containers.lis.prepend([1,2])
302 302 c.Containers.lis.extend([2,3])
303 303 obj = Containers(config=c)
304 304 self.assertEqual(obj.lis, [1,2,-1,2,3])
305 305
306 306 def test_append_extend(self):
307 307 c = Config()
308 308 c.Containers.lis.append([1,2])
309 309 c.Containers.lis.extend([2,3])
310 310 obj = Containers(config=c)
311 311 self.assertEqual(obj.lis, [-1,[1,2],2,3])
312 312
313 313 def test_extend_append(self):
314 314 c = Config()
315 315 c.Containers.lis.extend([2,3])
316 316 c.Containers.lis.append([1,2])
317 317 obj = Containers(config=c)
318 318 self.assertEqual(obj.lis, [-1,2,3,[1,2]])
319 319
320 320 def test_insert_extend(self):
321 321 c = Config()
322 322 c.Containers.lis.insert(0, 1)
323 323 c.Containers.lis.extend([2,3])
324 324 obj = Containers(config=c)
325 325 self.assertEqual(obj.lis, [1,-1,2,3])
326 326
327 327 def test_set_update(self):
328 328 c = Config()
329 329 c.Containers.s.update({0,1,2})
330 330 c.Containers.s.update({3})
331 331 obj = Containers(config=c)
332 332 self.assertEqual(obj.s, {'a', 0, 1, 2, 3})
333 333
334 334 def test_dict_update(self):
335 335 c = Config()
336 336 c.Containers.d.update({'c' : 'd'})
337 337 c.Containers.d.update({'e' : 'f'})
338 338 obj = Containers(config=c)
339 339 self.assertEqual(obj.d, {'a':'b', 'c':'d', 'e':'f'})
340 340
341 341 def test_update_twice(self):
342 342 c = Config()
343 343 c.MyConfigurable.a = 5
344 344 m = MyConfigurable(config=c)
345 345 self.assertEqual(m.a, 5)
346 346
347 347 c2 = Config()
348 348 c2.MyConfigurable.a = 10
349 349 m.update_config(c2)
350 350 self.assertEqual(m.a, 10)
351 351
352 352 c2.MyConfigurable.a = 15
353 353 m.update_config(c2)
354 354 self.assertEqual(m.a, 15)
355 355
356 356 def test_config_default(self):
357 357 class SomeSingleton(SingletonConfigurable):
358 358 pass
359 359
360 360 class DefaultConfigurable(Configurable):
361 361 a = Integer(config=True)
362 362 def _config_default(self):
363 363 if SomeSingleton.initialized():
364 364 return SomeSingleton.instance().config
365 365 return Config()
366 366
367 367 c = Config()
368 368 c.DefaultConfigurable.a = 5
369 369
370 370 d1 = DefaultConfigurable()
371 371 self.assertEqual(d1.a, 0)
372 372
373 373 single = SomeSingleton.instance(config=c)
374 374
375 375 d2 = DefaultConfigurable()
376 376 self.assertIs(d2.config, single.config)
377 377 self.assertEqual(d2.a, 5)
378 378
@@ -1,419 +1,419 b''
1 1 # encoding: utf-8
2 """Tests for IPython.config.loader"""
2 """Tests for traitlets.config.loader"""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import copy
8 8 import logging
9 9 import os
10 10 import pickle
11 11 import sys
12 12
13 13 from tempfile import mkstemp
14 14 from unittest import TestCase
15 15
16 16 from nose import SkipTest
17 17 import nose.tools as nt
18 18
19 19
20 20
21 from IPython.config.loader import (
21 from traitlets.config.loader import (
22 22 Config,
23 23 LazyConfigValue,
24 24 PyFileConfigLoader,
25 25 JSONFileConfigLoader,
26 26 KeyValueConfigLoader,
27 27 ArgParseConfigLoader,
28 28 KVArgParseConfigLoader,
29 29 ConfigError,
30 30 )
31 31
32 32
33 33 pyfile = """
34 34 c = get_config()
35 35 c.a=10
36 36 c.b=20
37 37 c.Foo.Bar.value=10
38 38 c.Foo.Bam.value=list(range(10))
39 39 c.D.C.value='hi there'
40 40 """
41 41
42 42 json1file = """
43 43 {
44 44 "version": 1,
45 45 "a": 10,
46 46 "b": 20,
47 47 "Foo": {
48 48 "Bam": {
49 49 "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
50 50 },
51 51 "Bar": {
52 52 "value": 10
53 53 }
54 54 },
55 55 "D": {
56 56 "C": {
57 57 "value": "hi there"
58 58 }
59 59 }
60 60 }
61 61 """
62 62
63 63 # should not load
64 64 json2file = """
65 65 {
66 66 "version": 2
67 67 }
68 68 """
69 69
70 70 import logging
71 71 log = logging.getLogger('devnull')
72 72 log.setLevel(0)
73 73
74 74 class TestFileCL(TestCase):
75 75
76 76 def _check_conf(self, config):
77 77 self.assertEqual(config.a, 10)
78 78 self.assertEqual(config.b, 20)
79 79 self.assertEqual(config.Foo.Bar.value, 10)
80 80 self.assertEqual(config.Foo.Bam.value, list(range(10)))
81 81 self.assertEqual(config.D.C.value, 'hi there')
82 82
83 83 def test_python(self):
84 84 fd, fname = mkstemp('.py')
85 85 f = os.fdopen(fd, 'w')
86 86 f.write(pyfile)
87 87 f.close()
88 88 # Unlink the file
89 89 cl = PyFileConfigLoader(fname, log=log)
90 90 config = cl.load_config()
91 91 self._check_conf(config)
92 92
93 93 def test_json(self):
94 94 fd, fname = mkstemp('.json')
95 95 f = os.fdopen(fd, 'w')
96 96 f.write(json1file)
97 97 f.close()
98 98 # Unlink the file
99 99 cl = JSONFileConfigLoader(fname, log=log)
100 100 config = cl.load_config()
101 101 self._check_conf(config)
102 102
103 103 def test_collision(self):
104 104 a = Config()
105 105 b = Config()
106 106 self.assertEqual(a.collisions(b), {})
107 107 a.A.trait1 = 1
108 108 b.A.trait2 = 2
109 109 self.assertEqual(a.collisions(b), {})
110 110 b.A.trait1 = 1
111 111 self.assertEqual(a.collisions(b), {})
112 112 b.A.trait1 = 0
113 113 self.assertEqual(a.collisions(b), {
114 114 'A': {
115 115 'trait1': "1 ignored, using 0",
116 116 }
117 117 })
118 118 self.assertEqual(b.collisions(a), {
119 119 'A': {
120 120 'trait1': "0 ignored, using 1",
121 121 }
122 122 })
123 123 a.A.trait2 = 3
124 124 self.assertEqual(b.collisions(a), {
125 125 'A': {
126 126 'trait1': "0 ignored, using 1",
127 127 'trait2': "2 ignored, using 3",
128 128 }
129 129 })
130 130
131 131 def test_v2raise(self):
132 132 fd, fname = mkstemp('.json')
133 133 f = os.fdopen(fd, 'w')
134 134 f.write(json2file)
135 135 f.close()
136 136 # Unlink the file
137 137 cl = JSONFileConfigLoader(fname, log=log)
138 138 with nt.assert_raises(ValueError):
139 139 cl.load_config()
140 140
141 141
142 142 class MyLoader1(ArgParseConfigLoader):
143 143 def _add_arguments(self, aliases=None, flags=None):
144 144 p = self.parser
145 145 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
146 146 p.add_argument('-b', dest='MyClass.bar', type=int)
147 147 p.add_argument('-n', dest='n', action='store_true')
148 148 p.add_argument('Global.bam', type=str)
149 149
150 150 class MyLoader2(ArgParseConfigLoader):
151 151 def _add_arguments(self, aliases=None, flags=None):
152 152 subparsers = self.parser.add_subparsers(dest='subparser_name')
153 153 subparser1 = subparsers.add_parser('1')
154 154 subparser1.add_argument('-x',dest='Global.x')
155 155 subparser2 = subparsers.add_parser('2')
156 156 subparser2.add_argument('y')
157 157
158 158 class TestArgParseCL(TestCase):
159 159
160 160 def test_basic(self):
161 161 cl = MyLoader1()
162 162 config = cl.load_config('-f hi -b 10 -n wow'.split())
163 163 self.assertEqual(config.Global.foo, 'hi')
164 164 self.assertEqual(config.MyClass.bar, 10)
165 165 self.assertEqual(config.n, True)
166 166 self.assertEqual(config.Global.bam, 'wow')
167 167 config = cl.load_config(['wow'])
168 168 self.assertEqual(list(config.keys()), ['Global'])
169 169 self.assertEqual(list(config.Global.keys()), ['bam'])
170 170 self.assertEqual(config.Global.bam, 'wow')
171 171
172 172 def test_add_arguments(self):
173 173 cl = MyLoader2()
174 174 config = cl.load_config('2 frobble'.split())
175 175 self.assertEqual(config.subparser_name, '2')
176 176 self.assertEqual(config.y, 'frobble')
177 177 config = cl.load_config('1 -x frobble'.split())
178 178 self.assertEqual(config.subparser_name, '1')
179 179 self.assertEqual(config.Global.x, 'frobble')
180 180
181 181 def test_argv(self):
182 182 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
183 183 config = cl.load_config()
184 184 self.assertEqual(config.Global.foo, 'hi')
185 185 self.assertEqual(config.MyClass.bar, 10)
186 186 self.assertEqual(config.n, True)
187 187 self.assertEqual(config.Global.bam, 'wow')
188 188
189 189
190 190 class TestKeyValueCL(TestCase):
191 191 klass = KeyValueConfigLoader
192 192
193 193 def test_eval(self):
194 194 cl = self.klass(log=log)
195 195 config = cl.load_config('--Class.str_trait=all --Class.int_trait=5 --Class.list_trait=["hello",5]'.split())
196 196 self.assertEqual(config.Class.str_trait, 'all')
197 197 self.assertEqual(config.Class.int_trait, 5)
198 198 self.assertEqual(config.Class.list_trait, ["hello", 5])
199 199
200 200 def test_basic(self):
201 201 cl = self.klass(log=log)
202 202 argv = [ '--' + s[2:] for s in pyfile.split('\n') if s.startswith('c.') ]
203 203 print(argv)
204 204 config = cl.load_config(argv)
205 205 self.assertEqual(config.a, 10)
206 206 self.assertEqual(config.b, 20)
207 207 self.assertEqual(config.Foo.Bar.value, 10)
208 208 # non-literal expressions are not evaluated
209 209 self.assertEqual(config.Foo.Bam.value, 'list(range(10))')
210 210 self.assertEqual(config.D.C.value, 'hi there')
211 211
212 212 def test_expanduser(self):
213 213 cl = self.klass(log=log)
214 214 argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
215 215 config = cl.load_config(argv)
216 216 self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
217 217 self.assertEqual(config.b, os.path.expanduser('~'))
218 218 self.assertEqual(config.c, os.path.expanduser('~/'))
219 219 self.assertEqual(config.d, '~/')
220 220
221 221 def test_extra_args(self):
222 222 cl = self.klass(log=log)
223 223 config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
224 224 self.assertEqual(cl.extra_args, ['b', 'd'])
225 225 self.assertEqual(config.a, 5)
226 226 self.assertEqual(config.c, 10)
227 227 config = cl.load_config(['--', '--a=5', '--c=10'])
228 228 self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
229 229
230 230 def test_unicode_args(self):
231 231 cl = self.klass(log=log)
232 232 argv = [u'--a=épsîlön']
233 233 config = cl.load_config(argv)
234 234 self.assertEqual(config.a, u'épsîlön')
235 235
236 236 def test_unicode_bytes_args(self):
237 237 uarg = u'--a=é'
238 238 try:
239 239 barg = uarg.encode(sys.stdin.encoding)
240 240 except (TypeError, UnicodeEncodeError):
241 241 raise SkipTest("sys.stdin.encoding can't handle 'é'")
242 242
243 243 cl = self.klass(log=log)
244 244 config = cl.load_config([barg])
245 245 self.assertEqual(config.a, u'é')
246 246
247 247 def test_unicode_alias(self):
248 248 cl = self.klass(log=log)
249 249 argv = [u'--a=épsîlön']
250 250 config = cl.load_config(argv, aliases=dict(a='A.a'))
251 251 self.assertEqual(config.A.a, u'épsîlön')
252 252
253 253
254 254 class TestArgParseKVCL(TestKeyValueCL):
255 255 klass = KVArgParseConfigLoader
256 256
257 257 def test_expanduser2(self):
258 258 cl = self.klass(log=log)
259 259 argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
260 260 config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
261 261 self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
262 262 self.assertEqual(config.A.b, '~/1/2/3')
263 263
264 264 def test_eval(self):
265 265 cl = self.klass(log=log)
266 266 argv = ['-c', 'a=5']
267 267 config = cl.load_config(argv, aliases=dict(c='A.c'))
268 268 self.assertEqual(config.A.c, u"a=5")
269 269
270 270
271 271 class TestConfig(TestCase):
272 272
273 273 def test_setget(self):
274 274 c = Config()
275 275 c.a = 10
276 276 self.assertEqual(c.a, 10)
277 277 self.assertEqual('b' in c, False)
278 278
279 279 def test_auto_section(self):
280 280 c = Config()
281 281 self.assertNotIn('A', c)
282 282 assert not c._has_section('A')
283 283 A = c.A
284 284 A.foo = 'hi there'
285 285 self.assertIn('A', c)
286 286 assert c._has_section('A')
287 287 self.assertEqual(c.A.foo, 'hi there')
288 288 del c.A
289 289 self.assertEqual(c.A, Config())
290 290
291 291 def test_merge_doesnt_exist(self):
292 292 c1 = Config()
293 293 c2 = Config()
294 294 c2.bar = 10
295 295 c2.Foo.bar = 10
296 296 c1.merge(c2)
297 297 self.assertEqual(c1.Foo.bar, 10)
298 298 self.assertEqual(c1.bar, 10)
299 299 c2.Bar.bar = 10
300 300 c1.merge(c2)
301 301 self.assertEqual(c1.Bar.bar, 10)
302 302
303 303 def test_merge_exists(self):
304 304 c1 = Config()
305 305 c2 = Config()
306 306 c1.Foo.bar = 10
307 307 c1.Foo.bam = 30
308 308 c2.Foo.bar = 20
309 309 c2.Foo.wow = 40
310 310 c1.merge(c2)
311 311 self.assertEqual(c1.Foo.bam, 30)
312 312 self.assertEqual(c1.Foo.bar, 20)
313 313 self.assertEqual(c1.Foo.wow, 40)
314 314 c2.Foo.Bam.bam = 10
315 315 c1.merge(c2)
316 316 self.assertEqual(c1.Foo.Bam.bam, 10)
317 317
318 318 def test_deepcopy(self):
319 319 c1 = Config()
320 320 c1.Foo.bar = 10
321 321 c1.Foo.bam = 30
322 322 c1.a = 'asdf'
323 323 c1.b = range(10)
324 324 c1.Test.logger = logging.Logger('test')
325 325 c1.Test.get_logger = logging.getLogger('test')
326 326 c2 = copy.deepcopy(c1)
327 327 self.assertEqual(c1, c2)
328 328 self.assertTrue(c1 is not c2)
329 329 self.assertTrue(c1.Foo is not c2.Foo)
330 330 self.assertTrue(c1.Test is not c2.Test)
331 331 self.assertTrue(c1.Test.logger is c2.Test.logger)
332 332 self.assertTrue(c1.Test.get_logger is c2.Test.get_logger)
333 333
334 334 def test_builtin(self):
335 335 c1 = Config()
336 336 c1.format = "json"
337 337
338 338 def test_fromdict(self):
339 339 c1 = Config({'Foo' : {'bar' : 1}})
340 340 self.assertEqual(c1.Foo.__class__, Config)
341 341 self.assertEqual(c1.Foo.bar, 1)
342 342
343 343 def test_fromdictmerge(self):
344 344 c1 = Config()
345 345 c2 = Config({'Foo' : {'bar' : 1}})
346 346 c1.merge(c2)
347 347 self.assertEqual(c1.Foo.__class__, Config)
348 348 self.assertEqual(c1.Foo.bar, 1)
349 349
350 350 def test_fromdictmerge2(self):
351 351 c1 = Config({'Foo' : {'baz' : 2}})
352 352 c2 = Config({'Foo' : {'bar' : 1}})
353 353 c1.merge(c2)
354 354 self.assertEqual(c1.Foo.__class__, Config)
355 355 self.assertEqual(c1.Foo.bar, 1)
356 356 self.assertEqual(c1.Foo.baz, 2)
357 357 self.assertNotIn('baz', c2.Foo)
358 358
359 359 def test_contains(self):
360 360 c1 = Config({'Foo' : {'baz' : 2}})
361 361 c2 = Config({'Foo' : {'bar' : 1}})
362 362 self.assertIn('Foo', c1)
363 363 self.assertIn('Foo.baz', c1)
364 364 self.assertIn('Foo.bar', c2)
365 365 self.assertNotIn('Foo.bar', c1)
366 366
367 367 def test_pickle_config(self):
368 368 cfg = Config()
369 369 cfg.Foo.bar = 1
370 370 pcfg = pickle.dumps(cfg)
371 371 cfg2 = pickle.loads(pcfg)
372 372 self.assertEqual(cfg2, cfg)
373 373
374 374 def test_getattr_section(self):
375 375 cfg = Config()
376 376 self.assertNotIn('Foo', cfg)
377 377 Foo = cfg.Foo
378 378 assert isinstance(Foo, Config)
379 379 self.assertIn('Foo', cfg)
380 380
381 381 def test_getitem_section(self):
382 382 cfg = Config()
383 383 self.assertNotIn('Foo', cfg)
384 384 Foo = cfg['Foo']
385 385 assert isinstance(Foo, Config)
386 386 self.assertIn('Foo', cfg)
387 387
388 388 def test_getattr_not_section(self):
389 389 cfg = Config()
390 390 self.assertNotIn('foo', cfg)
391 391 foo = cfg.foo
392 392 assert isinstance(foo, LazyConfigValue)
393 393 self.assertIn('foo', cfg)
394 394
395 395 def test_getattr_private_missing(self):
396 396 cfg = Config()
397 397 self.assertNotIn('_repr_html_', cfg)
398 398 with self.assertRaises(AttributeError):
399 399 _ = cfg._repr_html_
400 400 self.assertNotIn('_repr_html_', cfg)
401 401 self.assertEqual(len(cfg), 0)
402 402
403 403 def test_getitem_not_section(self):
404 404 cfg = Config()
405 405 self.assertNotIn('foo', cfg)
406 406 foo = cfg['foo']
407 407 assert isinstance(foo, LazyConfigValue)
408 408 self.assertIn('foo', cfg)
409 409
410 410 def test_merge_copies(self):
411 411 c = Config()
412 412 c2 = Config()
413 413 c2.Foo.trait = []
414 414 c.merge(c2)
415 415 c2.Foo.trait.append(1)
416 416 self.assertIsNot(c.Foo, c2.Foo)
417 417 self.assertEqual(c.Foo.trait, [])
418 418 self.assertEqual(c2.Foo.trait, [1])
419 419
1 NO CONTENT: file renamed from IPython/utils/tests/test_traitlets.py to traitlets/tests/test_traitlets.py
General Comments 0
You need to be logged in to leave comments. Login now