##// 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
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 """Test trait types of the widget packages."""
1 """Test trait types of the widget packages."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from unittest import TestCase
6 from unittest import TestCase
7 from IPython.utils.traitlets import HasTraits
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 from IPython.html.widgets import Color
9 from IPython.html.widgets import Color
10
10
11
11
12 class ColorTrait(HasTraits):
12 class ColorTrait(HasTraits):
13 value = Color("black")
13 value = Color("black")
14
14
15
15
16 class TestColor(TraitTestBase):
16 class TestColor(TraitTestBase):
17 obj = ColorTrait()
17 obj = ColorTrait()
18
18
19 _good_values = ["blue", "#AA0", "#FFFFFF"]
19 _good_values = ["blue", "#AA0", "#FFFFFF"]
20 _bad_values = ["vanilla", "blues"]
20 _bad_values = ["vanilla", "blues"]
@@ -1,520 +1,517 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Suite Runner.
2 """IPython Test Suite Runner.
3
3
4 This module provides a main entry point to a user script to test IPython
4 This module provides a main entry point to a user script to test IPython
5 itself from the command line. There are two ways of running this script:
5 itself from the command line. There are two ways of running this script:
6
6
7 1. With the syntax `iptest all`. This runs our entire test suite by
7 1. With the syntax `iptest all`. This runs our entire test suite by
8 calling this script (with different arguments) recursively. This
8 calling this script (with different arguments) recursively. This
9 causes modules and package to be tested in different processes, using nose
9 causes modules and package to be tested in different processes, using nose
10 or trial where appropriate.
10 or trial where appropriate.
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 the script simply calls nose, but with special command line flags and
12 the script simply calls nose, but with special command line flags and
13 plugins loaded.
13 plugins loaded.
14
14
15 """
15 """
16
16
17 # Copyright (c) IPython Development Team.
17 # Copyright (c) IPython Development Team.
18 # Distributed under the terms of the Modified BSD License.
18 # Distributed under the terms of the Modified BSD License.
19
19
20 from __future__ import print_function
20 from __future__ import print_function
21
21
22 import glob
22 import glob
23 from io import BytesIO
23 from io import BytesIO
24 import os
24 import os
25 import os.path as path
25 import os.path as path
26 import sys
26 import sys
27 from threading import Thread, Lock, Event
27 from threading import Thread, Lock, Event
28 import warnings
28 import warnings
29
29
30 import nose.plugins.builtin
30 import nose.plugins.builtin
31 from nose.plugins.xunit import Xunit
31 from nose.plugins.xunit import Xunit
32 from nose import SkipTest
32 from nose import SkipTest
33 from nose.core import TestProgram
33 from nose.core import TestProgram
34 from nose.plugins import Plugin
34 from nose.plugins import Plugin
35 from nose.util import safe_str
35 from nose.util import safe_str
36
36
37 from IPython.utils.process import is_cmd_found
37 from IPython.utils.process import is_cmd_found
38 from IPython.utils.py3compat import bytes_to_str
38 from IPython.utils.py3compat import bytes_to_str
39 from IPython.utils.importstring import import_item
39 from IPython.utils.importstring import import_item
40 from IPython.testing.plugin.ipdoctest import IPythonDoctest
40 from IPython.testing.plugin.ipdoctest import IPythonDoctest
41 from IPython.external.decorators import KnownFailure, knownfailureif
41 from IPython.external.decorators import KnownFailure, knownfailureif
42
42
43 pjoin = path.join
43 pjoin = path.join
44
44
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46 # Warnings control
46 # Warnings control
47 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
48
48
49 # Twisted generates annoying warnings with Python 2.6, as will do other code
49 # Twisted generates annoying warnings with Python 2.6, as will do other code
50 # that imports 'sets' as of today
50 # that imports 'sets' as of today
51 warnings.filterwarnings('ignore', 'the sets module is deprecated',
51 warnings.filterwarnings('ignore', 'the sets module is deprecated',
52 DeprecationWarning )
52 DeprecationWarning )
53
53
54 # This one also comes from Twisted
54 # This one also comes from Twisted
55 warnings.filterwarnings('ignore', 'the sha module is deprecated',
55 warnings.filterwarnings('ignore', 'the sha module is deprecated',
56 DeprecationWarning)
56 DeprecationWarning)
57
57
58 # Wx on Fedora11 spits these out
58 # Wx on Fedora11 spits these out
59 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
59 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
60 UserWarning)
60 UserWarning)
61
61
62 # ------------------------------------------------------------------------------
62 # ------------------------------------------------------------------------------
63 # Monkeypatch Xunit to count known failures as skipped.
63 # Monkeypatch Xunit to count known failures as skipped.
64 # ------------------------------------------------------------------------------
64 # ------------------------------------------------------------------------------
65 def monkeypatch_xunit():
65 def monkeypatch_xunit():
66 try:
66 try:
67 knownfailureif(True)(lambda: None)()
67 knownfailureif(True)(lambda: None)()
68 except Exception as e:
68 except Exception as e:
69 KnownFailureTest = type(e)
69 KnownFailureTest = type(e)
70
70
71 def addError(self, test, err, capt=None):
71 def addError(self, test, err, capt=None):
72 if issubclass(err[0], KnownFailureTest):
72 if issubclass(err[0], KnownFailureTest):
73 err = (SkipTest,) + err[1:]
73 err = (SkipTest,) + err[1:]
74 return self.orig_addError(test, err, capt)
74 return self.orig_addError(test, err, capt)
75
75
76 Xunit.orig_addError = Xunit.addError
76 Xunit.orig_addError = Xunit.addError
77 Xunit.addError = addError
77 Xunit.addError = addError
78
78
79 #-----------------------------------------------------------------------------
79 #-----------------------------------------------------------------------------
80 # Check which dependencies are installed and greater than minimum version.
80 # Check which dependencies are installed and greater than minimum version.
81 #-----------------------------------------------------------------------------
81 #-----------------------------------------------------------------------------
82 def extract_version(mod):
82 def extract_version(mod):
83 return mod.__version__
83 return mod.__version__
84
84
85 def test_for(item, min_version=None, callback=extract_version):
85 def test_for(item, min_version=None, callback=extract_version):
86 """Test to see if item is importable, and optionally check against a minimum
86 """Test to see if item is importable, and optionally check against a minimum
87 version.
87 version.
88
88
89 If min_version is given, the default behavior is to check against the
89 If min_version is given, the default behavior is to check against the
90 `__version__` attribute of the item, but specifying `callback` allows you to
90 `__version__` attribute of the item, but specifying `callback` allows you to
91 extract the value you are interested in. e.g::
91 extract the value you are interested in. e.g::
92
92
93 In [1]: import sys
93 In [1]: import sys
94
94
95 In [2]: from IPython.testing.iptest import test_for
95 In [2]: from IPython.testing.iptest import test_for
96
96
97 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
97 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
98 Out[3]: True
98 Out[3]: True
99
99
100 """
100 """
101 try:
101 try:
102 check = import_item(item)
102 check = import_item(item)
103 except (ImportError, RuntimeError):
103 except (ImportError, RuntimeError):
104 # GTK reports Runtime error if it can't be initialized even if it's
104 # GTK reports Runtime error if it can't be initialized even if it's
105 # importable.
105 # importable.
106 return False
106 return False
107 else:
107 else:
108 if min_version:
108 if min_version:
109 if callback:
109 if callback:
110 # extra processing step to get version to compare
110 # extra processing step to get version to compare
111 check = callback(check)
111 check = callback(check)
112
112
113 return check >= min_version
113 return check >= min_version
114 else:
114 else:
115 return True
115 return True
116
116
117 # Global dict where we can store information on what we have and what we don't
117 # Global dict where we can store information on what we have and what we don't
118 # have available at test run time
118 # have available at test run time
119 have = {}
119 have = {}
120
120
121 have['curses'] = test_for('_curses')
121 have['curses'] = test_for('_curses')
122 have['matplotlib'] = test_for('matplotlib')
122 have['matplotlib'] = test_for('matplotlib')
123 have['numpy'] = test_for('numpy')
123 have['numpy'] = test_for('numpy')
124 have['pexpect'] = test_for('pexpect')
124 have['pexpect'] = test_for('pexpect')
125 have['pymongo'] = test_for('pymongo')
125 have['pymongo'] = test_for('pymongo')
126 have['pygments'] = test_for('pygments')
126 have['pygments'] = test_for('pygments')
127 have['qt'] = test_for('IPython.external.qt')
127 have['qt'] = test_for('IPython.external.qt')
128 have['sqlite3'] = test_for('sqlite3')
128 have['sqlite3'] = test_for('sqlite3')
129 have['tornado'] = test_for('tornado.version_info', (4,0), callback=None)
129 have['tornado'] = test_for('tornado.version_info', (4,0), callback=None)
130 have['jinja2'] = test_for('jinja2')
130 have['jinja2'] = test_for('jinja2')
131 have['mistune'] = test_for('mistune')
131 have['mistune'] = test_for('mistune')
132 have['requests'] = test_for('requests')
132 have['requests'] = test_for('requests')
133 have['sphinx'] = test_for('sphinx')
133 have['sphinx'] = test_for('sphinx')
134 have['jsonschema'] = test_for('jsonschema')
134 have['jsonschema'] = test_for('jsonschema')
135 have['terminado'] = test_for('terminado')
135 have['terminado'] = test_for('terminado')
136 have['casperjs'] = is_cmd_found('casperjs')
136 have['casperjs'] = is_cmd_found('casperjs')
137 have['phantomjs'] = is_cmd_found('phantomjs')
137 have['phantomjs'] = is_cmd_found('phantomjs')
138 have['slimerjs'] = is_cmd_found('slimerjs')
138 have['slimerjs'] = is_cmd_found('slimerjs')
139
139
140 min_zmq = (13,)
140 min_zmq = (13,)
141
141
142 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
142 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
143
143
144 #-----------------------------------------------------------------------------
144 #-----------------------------------------------------------------------------
145 # Test suite definitions
145 # Test suite definitions
146 #-----------------------------------------------------------------------------
146 #-----------------------------------------------------------------------------
147
147
148 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
148 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
149 'extensions', 'lib', 'terminal', 'testing', 'utils',
149 'extensions', 'lib', 'terminal', 'testing', 'utils',
150 'nbformat', 'qt', 'html', 'nbconvert'
150 'nbformat', 'qt', 'html', 'nbconvert'
151 ]
151 ]
152
152
153 class TestSection(object):
153 class TestSection(object):
154 def __init__(self, name, includes):
154 def __init__(self, name, includes):
155 self.name = name
155 self.name = name
156 self.includes = includes
156 self.includes = includes
157 self.excludes = []
157 self.excludes = []
158 self.dependencies = []
158 self.dependencies = []
159 self.enabled = True
159 self.enabled = True
160
160
161 def exclude(self, module):
161 def exclude(self, module):
162 if not module.startswith('IPython'):
162 if not module.startswith('IPython'):
163 module = self.includes[0] + "." + module
163 module = self.includes[0] + "." + module
164 self.excludes.append(module.replace('.', os.sep))
164 self.excludes.append(module.replace('.', os.sep))
165
165
166 def requires(self, *packages):
166 def requires(self, *packages):
167 self.dependencies.extend(packages)
167 self.dependencies.extend(packages)
168
168
169 @property
169 @property
170 def will_run(self):
170 def will_run(self):
171 return self.enabled and all(have[p] for p in self.dependencies)
171 return self.enabled and all(have[p] for p in self.dependencies)
172
172
173 shims = {
173 shims = {
174 'parallel': 'ipython_parallel',
174 'parallel': 'ipython_parallel',
175 'kernel': 'ipython_kernel',
175 'kernel': 'ipython_kernel',
176 'kernel.inprocess': 'ipython_kernel.inprocess',
176 'kernel.inprocess': 'ipython_kernel.inprocess',
177 'config': 'traitlets',
177 }
178 }
178
179
179 # Name -> (include, exclude, dependencies_met)
180 # Name -> (include, exclude, dependencies_met)
180 test_sections = {n:TestSection(n, [shims.get(n, 'IPython.%s' % n)]) for n in test_group_names}
181 test_sections = {n:TestSection(n, [shims.get(n, 'IPython.%s' % n)]) for n in test_group_names}
181
182
182
183
183 # Exclusions and dependencies
184 # Exclusions and dependencies
184 # ---------------------------
185 # ---------------------------
185
186
186 # core:
187 # core:
187 sec = test_sections['core']
188 sec = test_sections['core']
188 if not have['sqlite3']:
189 if not have['sqlite3']:
189 sec.exclude('tests.test_history')
190 sec.exclude('tests.test_history')
190 sec.exclude('history')
191 sec.exclude('history')
191 if not have['matplotlib']:
192 if not have['matplotlib']:
192 sec.exclude('pylabtools'),
193 sec.exclude('pylabtools'),
193 sec.exclude('tests.test_pylabtools')
194 sec.exclude('tests.test_pylabtools')
194
195
195 # lib:
196 # lib:
196 sec = test_sections['lib']
197 sec = test_sections['lib']
197 if not have['zmq']:
198 if not have['zmq']:
198 sec.exclude('kernel')
199 sec.exclude('kernel')
199 # We do this unconditionally, so that the test suite doesn't import
200 # We do this unconditionally, so that the test suite doesn't import
200 # gtk, changing the default encoding and masking some unicode bugs.
201 # gtk, changing the default encoding and masking some unicode bugs.
201 sec.exclude('inputhookgtk')
202 sec.exclude('inputhookgtk')
202 # We also do this unconditionally, because wx can interfere with Unix signals.
203 # We also do this unconditionally, because wx can interfere with Unix signals.
203 # There are currently no tests for it anyway.
204 # There are currently no tests for it anyway.
204 sec.exclude('inputhookwx')
205 sec.exclude('inputhookwx')
205 # Testing inputhook will need a lot of thought, to figure out
206 # Testing inputhook will need a lot of thought, to figure out
206 # how to have tests that don't lock up with the gui event
207 # how to have tests that don't lock up with the gui event
207 # loops in the picture
208 # loops in the picture
208 sec.exclude('inputhook')
209 sec.exclude('inputhook')
209
210
210 # testing:
211 # testing:
211 sec = test_sections['testing']
212 sec = test_sections['testing']
212 # These have to be skipped on win32 because they use echo, rm, cd, etc.
213 # These have to be skipped on win32 because they use echo, rm, cd, etc.
213 # See ticket https://github.com/ipython/ipython/issues/87
214 # See ticket https://github.com/ipython/ipython/issues/87
214 if sys.platform == 'win32':
215 if sys.platform == 'win32':
215 sec.exclude('plugin.test_exampleip')
216 sec.exclude('plugin.test_exampleip')
216 sec.exclude('plugin.dtexample')
217 sec.exclude('plugin.dtexample')
217
218
218 # terminal:
219 # terminal:
219 if (not have['pexpect']) or (not have['zmq']):
220 if (not have['pexpect']) or (not have['zmq']):
220 test_sections['terminal'].exclude('console')
221 test_sections['terminal'].exclude('console')
221
222
222 # parallel
223 # parallel
223 sec = test_sections['parallel']
224 sec = test_sections['parallel']
224 sec.requires('zmq')
225 sec.requires('zmq')
225 if not have['pymongo']:
226 if not have['pymongo']:
226 sec.exclude('controller.mongodb')
227 sec.exclude('controller.mongodb')
227 sec.exclude('tests.test_mongodb')
228 sec.exclude('tests.test_mongodb')
228
229
229 # kernel:
230 # kernel:
230 sec = test_sections['kernel']
231 sec = test_sections['kernel']
231 sec.requires('zmq')
232 sec.requires('zmq')
232 # The in-process kernel tests are done in a separate section
233 # The in-process kernel tests are done in a separate section
233 sec.exclude('inprocess')
234 sec.exclude('inprocess')
234 # importing gtk sets the default encoding, which we want to avoid
235 # importing gtk sets the default encoding, which we want to avoid
235 sec.exclude('gui.gtkembed')
236 sec.exclude('gui.gtkembed')
236 sec.exclude('gui.gtk3embed')
237 sec.exclude('gui.gtk3embed')
237 if not have['matplotlib']:
238 if not have['matplotlib']:
238 sec.exclude('pylab')
239 sec.exclude('pylab')
239
240
240 # kernel.inprocess:
241 # kernel.inprocess:
241 test_sections['kernel.inprocess'].requires('zmq')
242 test_sections['kernel.inprocess'].requires('zmq')
242
243
243 # extensions:
244 # extensions:
244 sec = test_sections['extensions']
245 sec = test_sections['extensions']
245 # This is deprecated in favour of rpy2
246 # This is deprecated in favour of rpy2
246 sec.exclude('rmagic')
247 sec.exclude('rmagic')
247 # autoreload does some strange stuff, so move it to its own test section
248 # autoreload does some strange stuff, so move it to its own test section
248 sec.exclude('autoreload')
249 sec.exclude('autoreload')
249 sec.exclude('tests.test_autoreload')
250 sec.exclude('tests.test_autoreload')
250 test_sections['autoreload'] = TestSection('autoreload',
251 test_sections['autoreload'] = TestSection('autoreload',
251 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
252 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
252 test_group_names.append('autoreload')
253 test_group_names.append('autoreload')
253
254
254 # qt:
255 # qt:
255 test_sections['qt'].requires('zmq', 'qt', 'pygments')
256 test_sections['qt'].requires('zmq', 'qt', 'pygments')
256
257
257 # html:
258 # html:
258 sec = test_sections['html']
259 sec = test_sections['html']
259 sec.requires('zmq', 'tornado', 'requests', 'sqlite3', 'jsonschema')
260 sec.requires('zmq', 'tornado', 'requests', 'sqlite3', 'jsonschema')
260 # The notebook 'static' directory contains JS, css and other
261 # The notebook 'static' directory contains JS, css and other
261 # files for web serving. Occasionally projects may put a .py
262 # files for web serving. Occasionally projects may put a .py
262 # file in there (MathJax ships a conf.py), so we might as
263 # file in there (MathJax ships a conf.py), so we might as
263 # well play it safe and skip the whole thing.
264 # well play it safe and skip the whole thing.
264 sec.exclude('static')
265 sec.exclude('static')
265 sec.exclude('tasks')
266 sec.exclude('tasks')
266 if not have['jinja2']:
267 if not have['jinja2']:
267 sec.exclude('notebookapp')
268 sec.exclude('notebookapp')
268 if not have['pygments'] or not have['jinja2']:
269 if not have['pygments'] or not have['jinja2']:
269 sec.exclude('nbconvert')
270 sec.exclude('nbconvert')
270 if not have['terminado']:
271 if not have['terminado']:
271 sec.exclude('terminal')
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 # nbconvert:
274 # nbconvert:
278 sec = test_sections['nbconvert']
275 sec = test_sections['nbconvert']
279 sec.requires('pygments', 'jinja2', 'jsonschema', 'mistune')
276 sec.requires('pygments', 'jinja2', 'jsonschema', 'mistune')
280 # Exclude nbconvert directories containing config files used to test.
277 # Exclude nbconvert directories containing config files used to test.
281 # Executing the config files with iptest would cause an exception.
278 # Executing the config files with iptest would cause an exception.
282 sec.exclude('tests.files')
279 sec.exclude('tests.files')
283 sec.exclude('exporters.tests.files')
280 sec.exclude('exporters.tests.files')
284 if not have['tornado']:
281 if not have['tornado']:
285 sec.exclude('nbconvert.post_processors.serve')
282 sec.exclude('nbconvert.post_processors.serve')
286 sec.exclude('nbconvert.post_processors.tests.test_serve')
283 sec.exclude('nbconvert.post_processors.tests.test_serve')
287
284
288 # nbformat:
285 # nbformat:
289 test_sections['nbformat'].requires('jsonschema')
286 test_sections['nbformat'].requires('jsonschema')
290
287
291 #-----------------------------------------------------------------------------
288 #-----------------------------------------------------------------------------
292 # Functions and classes
289 # Functions and classes
293 #-----------------------------------------------------------------------------
290 #-----------------------------------------------------------------------------
294
291
295 def check_exclusions_exist():
292 def check_exclusions_exist():
296 from IPython.utils.path import get_ipython_package_dir
293 from IPython.utils.path import get_ipython_package_dir
297 from IPython.utils.warn import warn
294 from IPython.utils.warn import warn
298 parent = os.path.dirname(get_ipython_package_dir())
295 parent = os.path.dirname(get_ipython_package_dir())
299 for sec in test_sections:
296 for sec in test_sections:
300 for pattern in sec.exclusions:
297 for pattern in sec.exclusions:
301 fullpath = pjoin(parent, pattern)
298 fullpath = pjoin(parent, pattern)
302 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
299 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
303 warn("Excluding nonexistent file: %r" % pattern)
300 warn("Excluding nonexistent file: %r" % pattern)
304
301
305
302
306 class ExclusionPlugin(Plugin):
303 class ExclusionPlugin(Plugin):
307 """A nose plugin to effect our exclusions of files and directories.
304 """A nose plugin to effect our exclusions of files and directories.
308 """
305 """
309 name = 'exclusions'
306 name = 'exclusions'
310 score = 3000 # Should come before any other plugins
307 score = 3000 # Should come before any other plugins
311
308
312 def __init__(self, exclude_patterns=None):
309 def __init__(self, exclude_patterns=None):
313 """
310 """
314 Parameters
311 Parameters
315 ----------
312 ----------
316
313
317 exclude_patterns : sequence of strings, optional
314 exclude_patterns : sequence of strings, optional
318 Filenames containing these patterns (as raw strings, not as regular
315 Filenames containing these patterns (as raw strings, not as regular
319 expressions) are excluded from the tests.
316 expressions) are excluded from the tests.
320 """
317 """
321 self.exclude_patterns = exclude_patterns or []
318 self.exclude_patterns = exclude_patterns or []
322 super(ExclusionPlugin, self).__init__()
319 super(ExclusionPlugin, self).__init__()
323
320
324 def options(self, parser, env=os.environ):
321 def options(self, parser, env=os.environ):
325 Plugin.options(self, parser, env)
322 Plugin.options(self, parser, env)
326
323
327 def configure(self, options, config):
324 def configure(self, options, config):
328 Plugin.configure(self, options, config)
325 Plugin.configure(self, options, config)
329 # Override nose trying to disable plugin.
326 # Override nose trying to disable plugin.
330 self.enabled = True
327 self.enabled = True
331
328
332 def wantFile(self, filename):
329 def wantFile(self, filename):
333 """Return whether the given filename should be scanned for tests.
330 """Return whether the given filename should be scanned for tests.
334 """
331 """
335 if any(pat in filename for pat in self.exclude_patterns):
332 if any(pat in filename for pat in self.exclude_patterns):
336 return False
333 return False
337 return None
334 return None
338
335
339 def wantDirectory(self, directory):
336 def wantDirectory(self, directory):
340 """Return whether the given directory should be scanned for tests.
337 """Return whether the given directory should be scanned for tests.
341 """
338 """
342 if any(pat in directory for pat in self.exclude_patterns):
339 if any(pat in directory for pat in self.exclude_patterns):
343 return False
340 return False
344 return None
341 return None
345
342
346
343
347 class StreamCapturer(Thread):
344 class StreamCapturer(Thread):
348 daemon = True # Don't hang if main thread crashes
345 daemon = True # Don't hang if main thread crashes
349 started = False
346 started = False
350 def __init__(self, echo=False):
347 def __init__(self, echo=False):
351 super(StreamCapturer, self).__init__()
348 super(StreamCapturer, self).__init__()
352 self.echo = echo
349 self.echo = echo
353 self.streams = []
350 self.streams = []
354 self.buffer = BytesIO()
351 self.buffer = BytesIO()
355 self.readfd, self.writefd = os.pipe()
352 self.readfd, self.writefd = os.pipe()
356 self.buffer_lock = Lock()
353 self.buffer_lock = Lock()
357 self.stop = Event()
354 self.stop = Event()
358
355
359 def run(self):
356 def run(self):
360 self.started = True
357 self.started = True
361
358
362 while not self.stop.is_set():
359 while not self.stop.is_set():
363 chunk = os.read(self.readfd, 1024)
360 chunk = os.read(self.readfd, 1024)
364
361
365 with self.buffer_lock:
362 with self.buffer_lock:
366 self.buffer.write(chunk)
363 self.buffer.write(chunk)
367 if self.echo:
364 if self.echo:
368 sys.stdout.write(bytes_to_str(chunk))
365 sys.stdout.write(bytes_to_str(chunk))
369
366
370 os.close(self.readfd)
367 os.close(self.readfd)
371 os.close(self.writefd)
368 os.close(self.writefd)
372
369
373 def reset_buffer(self):
370 def reset_buffer(self):
374 with self.buffer_lock:
371 with self.buffer_lock:
375 self.buffer.truncate(0)
372 self.buffer.truncate(0)
376 self.buffer.seek(0)
373 self.buffer.seek(0)
377
374
378 def get_buffer(self):
375 def get_buffer(self):
379 with self.buffer_lock:
376 with self.buffer_lock:
380 return self.buffer.getvalue()
377 return self.buffer.getvalue()
381
378
382 def ensure_started(self):
379 def ensure_started(self):
383 if not self.started:
380 if not self.started:
384 self.start()
381 self.start()
385
382
386 def halt(self):
383 def halt(self):
387 """Safely stop the thread."""
384 """Safely stop the thread."""
388 if not self.started:
385 if not self.started:
389 return
386 return
390
387
391 self.stop.set()
388 self.stop.set()
392 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
389 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
393 self.join()
390 self.join()
394
391
395 class SubprocessStreamCapturePlugin(Plugin):
392 class SubprocessStreamCapturePlugin(Plugin):
396 name='subprocstreams'
393 name='subprocstreams'
397 def __init__(self):
394 def __init__(self):
398 Plugin.__init__(self)
395 Plugin.__init__(self)
399 self.stream_capturer = StreamCapturer()
396 self.stream_capturer = StreamCapturer()
400 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
397 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
401 # This is ugly, but distant parts of the test machinery need to be able
398 # This is ugly, but distant parts of the test machinery need to be able
402 # to redirect streams, so we make the object globally accessible.
399 # to redirect streams, so we make the object globally accessible.
403 nose.iptest_stdstreams_fileno = self.get_write_fileno
400 nose.iptest_stdstreams_fileno = self.get_write_fileno
404
401
405 def get_write_fileno(self):
402 def get_write_fileno(self):
406 if self.destination == 'capture':
403 if self.destination == 'capture':
407 self.stream_capturer.ensure_started()
404 self.stream_capturer.ensure_started()
408 return self.stream_capturer.writefd
405 return self.stream_capturer.writefd
409 elif self.destination == 'discard':
406 elif self.destination == 'discard':
410 return os.open(os.devnull, os.O_WRONLY)
407 return os.open(os.devnull, os.O_WRONLY)
411 else:
408 else:
412 return sys.__stdout__.fileno()
409 return sys.__stdout__.fileno()
413
410
414 def configure(self, options, config):
411 def configure(self, options, config):
415 Plugin.configure(self, options, config)
412 Plugin.configure(self, options, config)
416 # Override nose trying to disable plugin.
413 # Override nose trying to disable plugin.
417 if self.destination == 'capture':
414 if self.destination == 'capture':
418 self.enabled = True
415 self.enabled = True
419
416
420 def startTest(self, test):
417 def startTest(self, test):
421 # Reset log capture
418 # Reset log capture
422 self.stream_capturer.reset_buffer()
419 self.stream_capturer.reset_buffer()
423
420
424 def formatFailure(self, test, err):
421 def formatFailure(self, test, err):
425 # Show output
422 # Show output
426 ec, ev, tb = err
423 ec, ev, tb = err
427 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
424 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
428 if captured.strip():
425 if captured.strip():
429 ev = safe_str(ev)
426 ev = safe_str(ev)
430 out = [ev, '>> begin captured subprocess output <<',
427 out = [ev, '>> begin captured subprocess output <<',
431 captured,
428 captured,
432 '>> end captured subprocess output <<']
429 '>> end captured subprocess output <<']
433 return ec, '\n'.join(out), tb
430 return ec, '\n'.join(out), tb
434
431
435 return err
432 return err
436
433
437 formatError = formatFailure
434 formatError = formatFailure
438
435
439 def finalize(self, result):
436 def finalize(self, result):
440 self.stream_capturer.halt()
437 self.stream_capturer.halt()
441
438
442
439
443 def run_iptest():
440 def run_iptest():
444 """Run the IPython test suite using nose.
441 """Run the IPython test suite using nose.
445
442
446 This function is called when this script is **not** called with the form
443 This function is called when this script is **not** called with the form
447 `iptest all`. It simply calls nose with appropriate command line flags
444 `iptest all`. It simply calls nose with appropriate command line flags
448 and accepts all of the standard nose arguments.
445 and accepts all of the standard nose arguments.
449 """
446 """
450 # Apply our monkeypatch to Xunit
447 # Apply our monkeypatch to Xunit
451 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
448 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
452 monkeypatch_xunit()
449 monkeypatch_xunit()
453
450
454 warnings.filterwarnings('ignore',
451 warnings.filterwarnings('ignore',
455 'This will be removed soon. Use IPython.testing.util instead')
452 'This will be removed soon. Use IPython.testing.util instead')
456
453
457 arg1 = sys.argv[1]
454 arg1 = sys.argv[1]
458 if arg1 in test_sections:
455 if arg1 in test_sections:
459 section = test_sections[arg1]
456 section = test_sections[arg1]
460 sys.argv[1:2] = section.includes
457 sys.argv[1:2] = section.includes
461 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
458 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
462 section = test_sections[arg1[8:]]
459 section = test_sections[arg1[8:]]
463 sys.argv[1:2] = section.includes
460 sys.argv[1:2] = section.includes
464 else:
461 else:
465 section = TestSection(arg1, includes=[arg1])
462 section = TestSection(arg1, includes=[arg1])
466
463
467
464
468 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
465 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
469
466
470 '--with-ipdoctest',
467 '--with-ipdoctest',
471 '--ipdoctest-tests','--ipdoctest-extension=txt',
468 '--ipdoctest-tests','--ipdoctest-extension=txt',
472
469
473 # We add --exe because of setuptools' imbecility (it
470 # We add --exe because of setuptools' imbecility (it
474 # blindly does chmod +x on ALL files). Nose does the
471 # blindly does chmod +x on ALL files). Nose does the
475 # right thing and it tries to avoid executables,
472 # right thing and it tries to avoid executables,
476 # setuptools unfortunately forces our hand here. This
473 # setuptools unfortunately forces our hand here. This
477 # has been discussed on the distutils list and the
474 # has been discussed on the distutils list and the
478 # setuptools devs refuse to fix this problem!
475 # setuptools devs refuse to fix this problem!
479 '--exe',
476 '--exe',
480 ]
477 ]
481 if '-a' not in argv and '-A' not in argv:
478 if '-a' not in argv and '-A' not in argv:
482 argv = argv + ['-a', '!crash']
479 argv = argv + ['-a', '!crash']
483
480
484 if nose.__version__ >= '0.11':
481 if nose.__version__ >= '0.11':
485 # I don't fully understand why we need this one, but depending on what
482 # I don't fully understand why we need this one, but depending on what
486 # directory the test suite is run from, if we don't give it, 0 tests
483 # directory the test suite is run from, if we don't give it, 0 tests
487 # get run. Specifically, if the test suite is run from the source dir
484 # get run. Specifically, if the test suite is run from the source dir
488 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
485 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
489 # even if the same call done in this directory works fine). It appears
486 # even if the same call done in this directory works fine). It appears
490 # that if the requested package is in the current dir, nose bails early
487 # that if the requested package is in the current dir, nose bails early
491 # by default. Since it's otherwise harmless, leave it in by default
488 # by default. Since it's otherwise harmless, leave it in by default
492 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
489 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
493 argv.append('--traverse-namespace')
490 argv.append('--traverse-namespace')
494
491
495 # use our plugin for doctesting. It will remove the standard doctest plugin
492 # use our plugin for doctesting. It will remove the standard doctest plugin
496 # if it finds it enabled
493 # if it finds it enabled
497 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
494 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
498 SubprocessStreamCapturePlugin() ]
495 SubprocessStreamCapturePlugin() ]
499
496
500 # Use working directory set by parent process (see iptestcontroller)
497 # Use working directory set by parent process (see iptestcontroller)
501 if 'IPTEST_WORKING_DIR' in os.environ:
498 if 'IPTEST_WORKING_DIR' in os.environ:
502 os.chdir(os.environ['IPTEST_WORKING_DIR'])
499 os.chdir(os.environ['IPTEST_WORKING_DIR'])
503
500
504 # We need a global ipython running in this process, but the special
501 # We need a global ipython running in this process, but the special
505 # in-process group spawns its own IPython kernels, so for *that* group we
502 # in-process group spawns its own IPython kernels, so for *that* group we
506 # must avoid also opening the global one (otherwise there's a conflict of
503 # must avoid also opening the global one (otherwise there's a conflict of
507 # singletons). Ultimately the solution to this problem is to refactor our
504 # singletons). Ultimately the solution to this problem is to refactor our
508 # assumptions about what needs to be a singleton and what doesn't (app
505 # assumptions about what needs to be a singleton and what doesn't (app
509 # objects should, individual shells shouldn't). But for now, this
506 # objects should, individual shells shouldn't). But for now, this
510 # workaround allows the test suite for the inprocess module to complete.
507 # workaround allows the test suite for the inprocess module to complete.
511 if 'kernel.inprocess' not in section.name:
508 if 'kernel.inprocess' not in section.name:
512 from IPython.testing import globalipapp
509 from IPython.testing import globalipapp
513 globalipapp.start_ipython()
510 globalipapp.start_ipython()
514
511
515 # Now nose can run
512 # Now nose can run
516 TestProgram(argv=argv, addplugins=plugins)
513 TestProgram(argv=argv, addplugins=plugins)
517
514
518 if __name__ == '__main__':
515 if __name__ == '__main__':
519 run_iptest()
516 run_iptest()
520
517
@@ -1,495 +1,498 b''
1 """Generic testing tools.
1 """Generic testing tools.
2
2
3 Authors
3 Authors
4 -------
4 -------
5 - Fernando Perez <Fernando.Perez@berkeley.edu>
5 - Fernando Perez <Fernando.Perez@berkeley.edu>
6 """
6 """
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Copyright (C) 2009 The IPython Development Team
11 # Copyright (C) 2009 The IPython Development Team
12 #
12 #
13 # Distributed under the terms of the BSD License. The full license is in
13 # Distributed under the terms of the BSD License. The full license is in
14 # the file COPYING, distributed as part of this software.
14 # the file COPYING, distributed as part of this software.
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Imports
18 # Imports
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 import os
21 import os
22 import re
22 import re
23 import sys
23 import sys
24 import tempfile
24 import tempfile
25
25
26 from contextlib import contextmanager
26 from contextlib import contextmanager
27 from io import StringIO
27 from io import StringIO
28 from subprocess import Popen, PIPE
28 from subprocess import Popen, PIPE
29
29
30 try:
30 try:
31 # These tools are used by parts of the runtime, so we make the nose
31 # These tools are used by parts of the runtime, so we make the nose
32 # dependency optional at this point. Nose is a hard dependency to run the
32 # dependency optional at this point. Nose is a hard dependency to run the
33 # test suite, but NOT to use ipython itself.
33 # test suite, but NOT to use ipython itself.
34 import nose.tools as nt
34 import nose.tools as nt
35 has_nose = True
35 has_nose = True
36 except ImportError:
36 except ImportError:
37 has_nose = False
37 has_nose = False
38
38
39 from IPython.config.loader import Config
39 from IPython.config.loader import Config
40 from IPython.utils.process import get_output_error_code
40 from IPython.utils.process import get_output_error_code
41 from IPython.utils.text import list_strings
41 from IPython.utils.text import list_strings
42 from IPython.utils.io import temp_pyfile, Tee
42 from IPython.utils.io import temp_pyfile, Tee
43 from IPython.utils import py3compat
43 from IPython.utils import py3compat
44 from IPython.utils.encoding import DEFAULT_ENCODING
44 from IPython.utils.encoding import DEFAULT_ENCODING
45
45
46 from . import decorators as dec
46 from . import decorators as dec
47 from . import skipdoctest
47 from . import skipdoctest
48
48
49 #-----------------------------------------------------------------------------
49 #-----------------------------------------------------------------------------
50 # Functions and classes
50 # Functions and classes
51 #-----------------------------------------------------------------------------
51 #-----------------------------------------------------------------------------
52
52
53 # The docstring for full_path doctests differently on win32 (different path
53 # The docstring for full_path doctests differently on win32 (different path
54 # separator) so just skip the doctest there. The example remains informative.
54 # separator) so just skip the doctest there. The example remains informative.
55 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
55 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
56
56
57 @doctest_deco
57 @doctest_deco
58 def full_path(startPath,files):
58 def full_path(startPath,files):
59 """Make full paths for all the listed files, based on startPath.
59 """Make full paths for all the listed files, based on startPath.
60
60
61 Only the base part of startPath is kept, since this routine is typically
61 Only the base part of startPath is kept, since this routine is typically
62 used with a script's ``__file__`` variable as startPath. The base of startPath
62 used with a script's ``__file__`` variable as startPath. The base of startPath
63 is then prepended to all the listed files, forming the output list.
63 is then prepended to all the listed files, forming the output list.
64
64
65 Parameters
65 Parameters
66 ----------
66 ----------
67 startPath : string
67 startPath : string
68 Initial path to use as the base for the results. This path is split
68 Initial path to use as the base for the results. This path is split
69 using os.path.split() and only its first component is kept.
69 using os.path.split() and only its first component is kept.
70
70
71 files : string or list
71 files : string or list
72 One or more files.
72 One or more files.
73
73
74 Examples
74 Examples
75 --------
75 --------
76
76
77 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
77 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
78 ['/foo/a.txt', '/foo/b.txt']
78 ['/foo/a.txt', '/foo/b.txt']
79
79
80 >>> full_path('/foo',['a.txt','b.txt'])
80 >>> full_path('/foo',['a.txt','b.txt'])
81 ['/a.txt', '/b.txt']
81 ['/a.txt', '/b.txt']
82
82
83 If a single file is given, the output is still a list::
83 If a single file is given, the output is still a list::
84
84
85 >>> full_path('/foo','a.txt')
85 >>> full_path('/foo','a.txt')
86 ['/a.txt']
86 ['/a.txt']
87 """
87 """
88
88
89 files = list_strings(files)
89 files = list_strings(files)
90 base = os.path.split(startPath)[0]
90 base = os.path.split(startPath)[0]
91 return [ os.path.join(base,f) for f in files ]
91 return [ os.path.join(base,f) for f in files ]
92
92
93
93
94 def parse_test_output(txt):
94 def parse_test_output(txt):
95 """Parse the output of a test run and return errors, failures.
95 """Parse the output of a test run and return errors, failures.
96
96
97 Parameters
97 Parameters
98 ----------
98 ----------
99 txt : str
99 txt : str
100 Text output of a test run, assumed to contain a line of one of the
100 Text output of a test run, assumed to contain a line of one of the
101 following forms::
101 following forms::
102
102
103 'FAILED (errors=1)'
103 'FAILED (errors=1)'
104 'FAILED (failures=1)'
104 'FAILED (failures=1)'
105 'FAILED (errors=1, failures=1)'
105 'FAILED (errors=1, failures=1)'
106
106
107 Returns
107 Returns
108 -------
108 -------
109 nerr, nfail
109 nerr, nfail
110 number of errors and failures.
110 number of errors and failures.
111 """
111 """
112
112
113 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
113 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
114 if err_m:
114 if err_m:
115 nerr = int(err_m.group(1))
115 nerr = int(err_m.group(1))
116 nfail = 0
116 nfail = 0
117 return nerr, nfail
117 return nerr, nfail
118
118
119 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
119 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
120 if fail_m:
120 if fail_m:
121 nerr = 0
121 nerr = 0
122 nfail = int(fail_m.group(1))
122 nfail = int(fail_m.group(1))
123 return nerr, nfail
123 return nerr, nfail
124
124
125 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
125 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
126 re.MULTILINE)
126 re.MULTILINE)
127 if both_m:
127 if both_m:
128 nerr = int(both_m.group(1))
128 nerr = int(both_m.group(1))
129 nfail = int(both_m.group(2))
129 nfail = int(both_m.group(2))
130 return nerr, nfail
130 return nerr, nfail
131
131
132 # If the input didn't match any of these forms, assume no error/failures
132 # If the input didn't match any of these forms, assume no error/failures
133 return 0, 0
133 return 0, 0
134
134
135
135
136 # So nose doesn't think this is a test
136 # So nose doesn't think this is a test
137 parse_test_output.__test__ = False
137 parse_test_output.__test__ = False
138
138
139
139
140 def default_argv():
140 def default_argv():
141 """Return a valid default argv for creating testing instances of ipython"""
141 """Return a valid default argv for creating testing instances of ipython"""
142
142
143 return ['--quick', # so no config file is loaded
143 return ['--quick', # so no config file is loaded
144 # Other defaults to minimize side effects on stdout
144 # Other defaults to minimize side effects on stdout
145 '--colors=NoColor', '--no-term-title','--no-banner',
145 '--colors=NoColor', '--no-term-title','--no-banner',
146 '--autocall=0']
146 '--autocall=0']
147
147
148
148
149 def default_config():
149 def default_config():
150 """Return a config object with good defaults for testing."""
150 """Return a config object with good defaults for testing."""
151 config = Config()
151 config = Config()
152 config.TerminalInteractiveShell.colors = 'NoColor'
152 config.TerminalInteractiveShell.colors = 'NoColor'
153 config.TerminalTerminalInteractiveShell.term_title = False,
153 config.TerminalTerminalInteractiveShell.term_title = False,
154 config.TerminalInteractiveShell.autocall = 0
154 config.TerminalInteractiveShell.autocall = 0
155 f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
155 f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
156 config.HistoryManager.hist_file = f.name
156 config.HistoryManager.hist_file = f.name
157 f.close()
157 f.close()
158 config.HistoryManager.db_cache_size = 10000
158 config.HistoryManager.db_cache_size = 10000
159 return config
159 return config
160
160
161
161
162 def get_ipython_cmd(as_string=False):
162 def get_ipython_cmd(as_string=False):
163 """
163 """
164 Return appropriate IPython command line name. By default, this will return
164 Return appropriate IPython command line name. By default, this will return
165 a list that can be used with subprocess.Popen, for example, but passing
165 a list that can be used with subprocess.Popen, for example, but passing
166 `as_string=True` allows for returning the IPython command as a string.
166 `as_string=True` allows for returning the IPython command as a string.
167
167
168 Parameters
168 Parameters
169 ----------
169 ----------
170 as_string: bool
170 as_string: bool
171 Flag to allow to return the command as a string.
171 Flag to allow to return the command as a string.
172 """
172 """
173 ipython_cmd = [sys.executable, "-m", "IPython"]
173 ipython_cmd = [sys.executable, "-m", "IPython"]
174
174
175 if as_string:
175 if as_string:
176 ipython_cmd = " ".join(ipython_cmd)
176 ipython_cmd = " ".join(ipython_cmd)
177
177
178 return ipython_cmd
178 return ipython_cmd
179
179
180 def ipexec(fname, options=None, commands=()):
180 def ipexec(fname, options=None, commands=()):
181 """Utility to call 'ipython filename'.
181 """Utility to call 'ipython filename'.
182
182
183 Starts IPython with a minimal and safe configuration to make startup as fast
183 Starts IPython with a minimal and safe configuration to make startup as fast
184 as possible.
184 as possible.
185
185
186 Note that this starts IPython in a subprocess!
186 Note that this starts IPython in a subprocess!
187
187
188 Parameters
188 Parameters
189 ----------
189 ----------
190 fname : str
190 fname : str
191 Name of file to be executed (should have .py or .ipy extension).
191 Name of file to be executed (should have .py or .ipy extension).
192
192
193 options : optional, list
193 options : optional, list
194 Extra command-line flags to be passed to IPython.
194 Extra command-line flags to be passed to IPython.
195
195
196 commands : optional, list
196 commands : optional, list
197 Commands to send in on stdin
197 Commands to send in on stdin
198
198
199 Returns
199 Returns
200 -------
200 -------
201 (stdout, stderr) of ipython subprocess.
201 (stdout, stderr) of ipython subprocess.
202 """
202 """
203 if options is None: options = []
203 if options is None: options = []
204
204
205 # For these subprocess calls, eliminate all prompt printing so we only see
205 # For these subprocess calls, eliminate all prompt printing so we only see
206 # output from script execution
206 # output from script execution
207 prompt_opts = [ '--PromptManager.in_template=""',
207 prompt_opts = [ '--PromptManager.in_template=""',
208 '--PromptManager.in2_template=""',
208 '--PromptManager.in2_template=""',
209 '--PromptManager.out_template=""'
209 '--PromptManager.out_template=""'
210 ]
210 ]
211 cmdargs = default_argv() + prompt_opts + options
211 cmdargs = default_argv() + prompt_opts + options
212
212
213 test_dir = os.path.dirname(__file__)
213 test_dir = os.path.dirname(__file__)
214
214
215 ipython_cmd = get_ipython_cmd()
215 ipython_cmd = get_ipython_cmd()
216 # Absolute path for filename
216 # Absolute path for filename
217 full_fname = os.path.join(test_dir, fname)
217 full_fname = os.path.join(test_dir, fname)
218 full_cmd = ipython_cmd + cmdargs + [full_fname]
218 full_cmd = ipython_cmd + cmdargs + [full_fname]
219 env = os.environ.copy()
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 for k, v in env.items():
224 for k, v in env.items():
222 # Debug a bizarre failure we've seen on Windows:
225 # Debug a bizarre failure we've seen on Windows:
223 # TypeError: environment can only contain strings
226 # TypeError: environment can only contain strings
224 if not isinstance(v, str):
227 if not isinstance(v, str):
225 print(k, v)
228 print(k, v)
226 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
229 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
227 out, err = p.communicate(input=py3compat.str_to_bytes('\n'.join(commands)) or None)
230 out, err = p.communicate(input=py3compat.str_to_bytes('\n'.join(commands)) or None)
228 out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err)
231 out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err)
229 # `import readline` causes 'ESC[?1034h' to be output sometimes,
232 # `import readline` causes 'ESC[?1034h' to be output sometimes,
230 # so strip that out before doing comparisons
233 # so strip that out before doing comparisons
231 if out:
234 if out:
232 out = re.sub(r'\x1b\[[^h]+h', '', out)
235 out = re.sub(r'\x1b\[[^h]+h', '', out)
233 return out, err
236 return out, err
234
237
235
238
236 def ipexec_validate(fname, expected_out, expected_err='',
239 def ipexec_validate(fname, expected_out, expected_err='',
237 options=None, commands=()):
240 options=None, commands=()):
238 """Utility to call 'ipython filename' and validate output/error.
241 """Utility to call 'ipython filename' and validate output/error.
239
242
240 This function raises an AssertionError if the validation fails.
243 This function raises an AssertionError if the validation fails.
241
244
242 Note that this starts IPython in a subprocess!
245 Note that this starts IPython in a subprocess!
243
246
244 Parameters
247 Parameters
245 ----------
248 ----------
246 fname : str
249 fname : str
247 Name of the file to be executed (should have .py or .ipy extension).
250 Name of the file to be executed (should have .py or .ipy extension).
248
251
249 expected_out : str
252 expected_out : str
250 Expected stdout of the process.
253 Expected stdout of the process.
251
254
252 expected_err : optional, str
255 expected_err : optional, str
253 Expected stderr of the process.
256 Expected stderr of the process.
254
257
255 options : optional, list
258 options : optional, list
256 Extra command-line flags to be passed to IPython.
259 Extra command-line flags to be passed to IPython.
257
260
258 Returns
261 Returns
259 -------
262 -------
260 None
263 None
261 """
264 """
262
265
263 import nose.tools as nt
266 import nose.tools as nt
264
267
265 out, err = ipexec(fname, options, commands)
268 out, err = ipexec(fname, options, commands)
266 #print 'OUT', out # dbg
269 #print 'OUT', out # dbg
267 #print 'ERR', err # dbg
270 #print 'ERR', err # dbg
268 # If there are any errors, we must check those befor stdout, as they may be
271 # If there are any errors, we must check those befor stdout, as they may be
269 # more informative than simply having an empty stdout.
272 # more informative than simply having an empty stdout.
270 if err:
273 if err:
271 if expected_err:
274 if expected_err:
272 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
275 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
273 else:
276 else:
274 raise ValueError('Running file %r produced error: %r' %
277 raise ValueError('Running file %r produced error: %r' %
275 (fname, err))
278 (fname, err))
276 # If no errors or output on stderr was expected, match stdout
279 # If no errors or output on stderr was expected, match stdout
277 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
280 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
278
281
279
282
280 class TempFileMixin(object):
283 class TempFileMixin(object):
281 """Utility class to create temporary Python/IPython files.
284 """Utility class to create temporary Python/IPython files.
282
285
283 Meant as a mixin class for test cases."""
286 Meant as a mixin class for test cases."""
284
287
285 def mktmp(self, src, ext='.py'):
288 def mktmp(self, src, ext='.py'):
286 """Make a valid python temp file."""
289 """Make a valid python temp file."""
287 fname, f = temp_pyfile(src, ext)
290 fname, f = temp_pyfile(src, ext)
288 self.tmpfile = f
291 self.tmpfile = f
289 self.fname = fname
292 self.fname = fname
290
293
291 def tearDown(self):
294 def tearDown(self):
292 if hasattr(self, 'tmpfile'):
295 if hasattr(self, 'tmpfile'):
293 # If the tmpfile wasn't made because of skipped tests, like in
296 # If the tmpfile wasn't made because of skipped tests, like in
294 # win32, there's nothing to cleanup.
297 # win32, there's nothing to cleanup.
295 self.tmpfile.close()
298 self.tmpfile.close()
296 try:
299 try:
297 os.unlink(self.fname)
300 os.unlink(self.fname)
298 except:
301 except:
299 # On Windows, even though we close the file, we still can't
302 # On Windows, even though we close the file, we still can't
300 # delete it. I have no clue why
303 # delete it. I have no clue why
301 pass
304 pass
302
305
303 pair_fail_msg = ("Testing {0}\n\n"
306 pair_fail_msg = ("Testing {0}\n\n"
304 "In:\n"
307 "In:\n"
305 " {1!r}\n"
308 " {1!r}\n"
306 "Expected:\n"
309 "Expected:\n"
307 " {2!r}\n"
310 " {2!r}\n"
308 "Got:\n"
311 "Got:\n"
309 " {3!r}\n")
312 " {3!r}\n")
310 def check_pairs(func, pairs):
313 def check_pairs(func, pairs):
311 """Utility function for the common case of checking a function with a
314 """Utility function for the common case of checking a function with a
312 sequence of input/output pairs.
315 sequence of input/output pairs.
313
316
314 Parameters
317 Parameters
315 ----------
318 ----------
316 func : callable
319 func : callable
317 The function to be tested. Should accept a single argument.
320 The function to be tested. Should accept a single argument.
318 pairs : iterable
321 pairs : iterable
319 A list of (input, expected_output) tuples.
322 A list of (input, expected_output) tuples.
320
323
321 Returns
324 Returns
322 -------
325 -------
323 None. Raises an AssertionError if any output does not match the expected
326 None. Raises an AssertionError if any output does not match the expected
324 value.
327 value.
325 """
328 """
326 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
329 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
327 for inp, expected in pairs:
330 for inp, expected in pairs:
328 out = func(inp)
331 out = func(inp)
329 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
332 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
330
333
331
334
332 if py3compat.PY3:
335 if py3compat.PY3:
333 MyStringIO = StringIO
336 MyStringIO = StringIO
334 else:
337 else:
335 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
338 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
336 # so we need a class that can handle both.
339 # so we need a class that can handle both.
337 class MyStringIO(StringIO):
340 class MyStringIO(StringIO):
338 def write(self, s):
341 def write(self, s):
339 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
342 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
340 super(MyStringIO, self).write(s)
343 super(MyStringIO, self).write(s)
341
344
342 _re_type = type(re.compile(r''))
345 _re_type = type(re.compile(r''))
343
346
344 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
347 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
345 -------
348 -------
346 {2!s}
349 {2!s}
347 -------
350 -------
348 """
351 """
349
352
350 class AssertPrints(object):
353 class AssertPrints(object):
351 """Context manager for testing that code prints certain text.
354 """Context manager for testing that code prints certain text.
352
355
353 Examples
356 Examples
354 --------
357 --------
355 >>> with AssertPrints("abc", suppress=False):
358 >>> with AssertPrints("abc", suppress=False):
356 ... print("abcd")
359 ... print("abcd")
357 ... print("def")
360 ... print("def")
358 ...
361 ...
359 abcd
362 abcd
360 def
363 def
361 """
364 """
362 def __init__(self, s, channel='stdout', suppress=True):
365 def __init__(self, s, channel='stdout', suppress=True):
363 self.s = s
366 self.s = s
364 if isinstance(self.s, (py3compat.string_types, _re_type)):
367 if isinstance(self.s, (py3compat.string_types, _re_type)):
365 self.s = [self.s]
368 self.s = [self.s]
366 self.channel = channel
369 self.channel = channel
367 self.suppress = suppress
370 self.suppress = suppress
368
371
369 def __enter__(self):
372 def __enter__(self):
370 self.orig_stream = getattr(sys, self.channel)
373 self.orig_stream = getattr(sys, self.channel)
371 self.buffer = MyStringIO()
374 self.buffer = MyStringIO()
372 self.tee = Tee(self.buffer, channel=self.channel)
375 self.tee = Tee(self.buffer, channel=self.channel)
373 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
376 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
374
377
375 def __exit__(self, etype, value, traceback):
378 def __exit__(self, etype, value, traceback):
376 try:
379 try:
377 if value is not None:
380 if value is not None:
378 # If an error was raised, don't check anything else
381 # If an error was raised, don't check anything else
379 return False
382 return False
380 self.tee.flush()
383 self.tee.flush()
381 setattr(sys, self.channel, self.orig_stream)
384 setattr(sys, self.channel, self.orig_stream)
382 printed = self.buffer.getvalue()
385 printed = self.buffer.getvalue()
383 for s in self.s:
386 for s in self.s:
384 if isinstance(s, _re_type):
387 if isinstance(s, _re_type):
385 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
388 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
386 else:
389 else:
387 assert s in printed, notprinted_msg.format(s, self.channel, printed)
390 assert s in printed, notprinted_msg.format(s, self.channel, printed)
388 return False
391 return False
389 finally:
392 finally:
390 self.tee.close()
393 self.tee.close()
391
394
392 printed_msg = """Found {0!r} in printed output (on {1}):
395 printed_msg = """Found {0!r} in printed output (on {1}):
393 -------
396 -------
394 {2!s}
397 {2!s}
395 -------
398 -------
396 """
399 """
397
400
398 class AssertNotPrints(AssertPrints):
401 class AssertNotPrints(AssertPrints):
399 """Context manager for checking that certain output *isn't* produced.
402 """Context manager for checking that certain output *isn't* produced.
400
403
401 Counterpart of AssertPrints"""
404 Counterpart of AssertPrints"""
402 def __exit__(self, etype, value, traceback):
405 def __exit__(self, etype, value, traceback):
403 try:
406 try:
404 if value is not None:
407 if value is not None:
405 # If an error was raised, don't check anything else
408 # If an error was raised, don't check anything else
406 self.tee.close()
409 self.tee.close()
407 return False
410 return False
408 self.tee.flush()
411 self.tee.flush()
409 setattr(sys, self.channel, self.orig_stream)
412 setattr(sys, self.channel, self.orig_stream)
410 printed = self.buffer.getvalue()
413 printed = self.buffer.getvalue()
411 for s in self.s:
414 for s in self.s:
412 if isinstance(s, _re_type):
415 if isinstance(s, _re_type):
413 assert not s.search(printed),printed_msg.format(
416 assert not s.search(printed),printed_msg.format(
414 s.pattern, self.channel, printed)
417 s.pattern, self.channel, printed)
415 else:
418 else:
416 assert s not in printed, printed_msg.format(
419 assert s not in printed, printed_msg.format(
417 s, self.channel, printed)
420 s, self.channel, printed)
418 return False
421 return False
419 finally:
422 finally:
420 self.tee.close()
423 self.tee.close()
421
424
422 @contextmanager
425 @contextmanager
423 def mute_warn():
426 def mute_warn():
424 from IPython.utils import warn
427 from IPython.utils import warn
425 save_warn = warn.warn
428 save_warn = warn.warn
426 warn.warn = lambda *a, **kw: None
429 warn.warn = lambda *a, **kw: None
427 try:
430 try:
428 yield
431 yield
429 finally:
432 finally:
430 warn.warn = save_warn
433 warn.warn = save_warn
431
434
432 @contextmanager
435 @contextmanager
433 def make_tempfile(name):
436 def make_tempfile(name):
434 """ Create an empty, named, temporary file for the duration of the context.
437 """ Create an empty, named, temporary file for the duration of the context.
435 """
438 """
436 f = open(name, 'w')
439 f = open(name, 'w')
437 f.close()
440 f.close()
438 try:
441 try:
439 yield
442 yield
440 finally:
443 finally:
441 os.unlink(name)
444 os.unlink(name)
442
445
443
446
444 @contextmanager
447 @contextmanager
445 def monkeypatch(obj, name, attr):
448 def monkeypatch(obj, name, attr):
446 """
449 """
447 Context manager to replace attribute named `name` in `obj` with `attr`.
450 Context manager to replace attribute named `name` in `obj` with `attr`.
448 """
451 """
449 orig = getattr(obj, name)
452 orig = getattr(obj, name)
450 setattr(obj, name, attr)
453 setattr(obj, name, attr)
451 yield
454 yield
452 setattr(obj, name, orig)
455 setattr(obj, name, orig)
453
456
454
457
455 def help_output_test(subcommand=''):
458 def help_output_test(subcommand=''):
456 """test that `ipython [subcommand] -h` works"""
459 """test that `ipython [subcommand] -h` works"""
457 cmd = get_ipython_cmd() + [subcommand, '-h']
460 cmd = get_ipython_cmd() + [subcommand, '-h']
458 out, err, rc = get_output_error_code(cmd)
461 out, err, rc = get_output_error_code(cmd)
459 nt.assert_equal(rc, 0, err)
462 nt.assert_equal(rc, 0, err)
460 nt.assert_not_in("Traceback", err)
463 nt.assert_not_in("Traceback", err)
461 nt.assert_in("Options", out)
464 nt.assert_in("Options", out)
462 nt.assert_in("--help-all", out)
465 nt.assert_in("--help-all", out)
463 return out, err
466 return out, err
464
467
465
468
466 def help_all_output_test(subcommand=''):
469 def help_all_output_test(subcommand=''):
467 """test that `ipython [subcommand] --help-all` works"""
470 """test that `ipython [subcommand] --help-all` works"""
468 cmd = get_ipython_cmd() + [subcommand, '--help-all']
471 cmd = get_ipython_cmd() + [subcommand, '--help-all']
469 out, err, rc = get_output_error_code(cmd)
472 out, err, rc = get_output_error_code(cmd)
470 nt.assert_equal(rc, 0, err)
473 nt.assert_equal(rc, 0, err)
471 nt.assert_not_in("Traceback", err)
474 nt.assert_not_in("Traceback", err)
472 nt.assert_in("Options", out)
475 nt.assert_in("Options", out)
473 nt.assert_in("Class parameters", out)
476 nt.assert_in("Class parameters", out)
474 return out, err
477 return out, err
475
478
476 def assert_big_text_equal(a, b, chunk_size=80):
479 def assert_big_text_equal(a, b, chunk_size=80):
477 """assert that large strings are equal
480 """assert that large strings are equal
478
481
479 Zooms in on first chunk that differs,
482 Zooms in on first chunk that differs,
480 to give better info than vanilla assertEqual for large text blobs.
483 to give better info than vanilla assertEqual for large text blobs.
481 """
484 """
482 for i in range(0, len(a), chunk_size):
485 for i in range(0, len(a), chunk_size):
483 chunk_a = a[i:i + chunk_size]
486 chunk_a = a[i:i + chunk_size]
484 chunk_b = b[i:i + chunk_size]
487 chunk_b = b[i:i + chunk_size]
485 nt.assert_equal(chunk_a, chunk_b, "[offset: %i]\n%r != \n%r" % (
488 nt.assert_equal(chunk_a, chunk_b, "[offset: %i]\n%r != \n%r" % (
486 i, chunk_a, chunk_b))
489 i, chunk_a, chunk_b))
487
490
488 if len(a) > len(b):
491 if len(a) > len(b):
489 nt.fail("Length doesn't match (%i > %i). Extra text:\n%r" % (
492 nt.fail("Length doesn't match (%i > %i). Extra text:\n%r" % (
490 len(a), len(b), a[len(b):]
493 len(a), len(b), a[len(b):]
491 ))
494 ))
492 elif len(a) < len(b):
495 elif len(a) < len(b):
493 nt.fail("Length doesn't match (%i < %i). Extra text:\n%r" % (
496 nt.fail("Length doesn't match (%i < %i). Extra text:\n%r" % (
494 len(a), len(b), b[len(a):]
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
1 from __future__ import absolute_import
2 """
3 A lightweight Traits like module.
4
2
5 This is designed to provide a lightweight, simple, pure Python version of
3 from traitlets import *
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)
1 NO CONTENT: file renamed from IPython/config/__init__.py to traitlets/config/__init__.py
NO CONTENT: file renamed from IPython/config/__init__.py to traitlets/config/__init__.py
@@ -1,622 +1,622 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """A base class for a configurable application."""
2 """A base class for a configurable application."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import json
9 import json
10 import logging
10 import logging
11 import os
11 import os
12 import re
12 import re
13 import sys
13 import sys
14 from copy import deepcopy
14 from copy import deepcopy
15 from collections import defaultdict
15 from collections import defaultdict
16
16
17 from decorator import decorator
17 from decorator import decorator
18
18
19 from IPython.config.configurable import SingletonConfigurable
19 from traitlets.config.configurable import SingletonConfigurable
20 from IPython.config.loader import (
20 from traitlets.config.loader import (
21 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, JSONFileConfigLoader
21 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, JSONFileConfigLoader
22 )
22 )
23
23
24 from IPython.utils.traitlets import (
24 from traitlets.traitlets import (
25 Unicode, List, Enum, Dict, Instance, TraitError
25 Unicode, List, Enum, Dict, Instance, TraitError
26 )
26 )
27 from IPython.utils.importstring import import_item
27 from IPython.utils.importstring import import_item
28 from IPython.utils.text import indent, wrap_paragraphs, dedent
28 from IPython.utils.text import indent, wrap_paragraphs, dedent
29 from IPython.utils import py3compat
29 from IPython.utils import py3compat
30 from IPython.utils.py3compat import string_types, iteritems
30 from IPython.utils.py3compat import string_types, iteritems
31
31
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 # Descriptions for the various sections
33 # Descriptions for the various sections
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35
35
36 # merge flags&aliases into options
36 # merge flags&aliases into options
37 option_description = """
37 option_description = """
38 Arguments that take values are actually convenience aliases to full
38 Arguments that take values are actually convenience aliases to full
39 Configurables, whose aliases are listed on the help line. For more information
39 Configurables, whose aliases are listed on the help line. For more information
40 on full configurables, see '--help-all'.
40 on full configurables, see '--help-all'.
41 """.strip() # trim newlines of front and back
41 """.strip() # trim newlines of front and back
42
42
43 keyvalue_description = """
43 keyvalue_description = """
44 Parameters are set from command-line arguments of the form:
44 Parameters are set from command-line arguments of the form:
45 `--Class.trait=value`.
45 `--Class.trait=value`.
46 This line is evaluated in Python, so simple expressions are allowed, e.g.::
46 This line is evaluated in Python, so simple expressions are allowed, e.g.::
47 `--C.a='range(3)'` For setting C.a=[0,1,2].
47 `--C.a='range(3)'` For setting C.a=[0,1,2].
48 """.strip() # trim newlines of front and back
48 """.strip() # trim newlines of front and back
49
49
50 # sys.argv can be missing, for example when python is embedded. See the docs
50 # sys.argv can be missing, for example when python is embedded. See the docs
51 # for details: http://docs.python.org/2/c-api/intro.html#embedding-python
51 # for details: http://docs.python.org/2/c-api/intro.html#embedding-python
52 if not hasattr(sys, "argv"):
52 if not hasattr(sys, "argv"):
53 sys.argv = [""]
53 sys.argv = [""]
54
54
55 subcommand_description = """
55 subcommand_description = """
56 Subcommands are launched as `{app} cmd [args]`. For information on using
56 Subcommands are launched as `{app} cmd [args]`. For information on using
57 subcommand 'cmd', do: `{app} cmd -h`.
57 subcommand 'cmd', do: `{app} cmd -h`.
58 """
58 """
59 # get running program name
59 # get running program name
60
60
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62 # Application class
62 # Application class
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64
64
65 @decorator
65 @decorator
66 def catch_config_error(method, app, *args, **kwargs):
66 def catch_config_error(method, app, *args, **kwargs):
67 """Method decorator for catching invalid config (Trait/ArgumentErrors) during init.
67 """Method decorator for catching invalid config (Trait/ArgumentErrors) during init.
68
68
69 On a TraitError (generally caused by bad config), this will print the trait's
69 On a TraitError (generally caused by bad config), this will print the trait's
70 message, and exit the app.
70 message, and exit the app.
71
71
72 For use on init methods, to prevent invoking excepthook on invalid input.
72 For use on init methods, to prevent invoking excepthook on invalid input.
73 """
73 """
74 try:
74 try:
75 return method(app, *args, **kwargs)
75 return method(app, *args, **kwargs)
76 except (TraitError, ArgumentError) as e:
76 except (TraitError, ArgumentError) as e:
77 app.print_help()
77 app.print_help()
78 app.log.fatal("Bad config encountered during initialization:")
78 app.log.fatal("Bad config encountered during initialization:")
79 app.log.fatal(str(e))
79 app.log.fatal(str(e))
80 app.log.debug("Config at the time: %s", app.config)
80 app.log.debug("Config at the time: %s", app.config)
81 app.exit(1)
81 app.exit(1)
82
82
83
83
84 class ApplicationError(Exception):
84 class ApplicationError(Exception):
85 pass
85 pass
86
86
87 class LevelFormatter(logging.Formatter):
87 class LevelFormatter(logging.Formatter):
88 """Formatter with additional `highlevel` record
88 """Formatter with additional `highlevel` record
89
89
90 This field is empty if log level is less than highlevel_limit,
90 This field is empty if log level is less than highlevel_limit,
91 otherwise it is formatted with self.highlevel_format.
91 otherwise it is formatted with self.highlevel_format.
92
92
93 Useful for adding 'WARNING' to warning messages,
93 Useful for adding 'WARNING' to warning messages,
94 without adding 'INFO' to info, etc.
94 without adding 'INFO' to info, etc.
95 """
95 """
96 highlevel_limit = logging.WARN
96 highlevel_limit = logging.WARN
97 highlevel_format = " %(levelname)s |"
97 highlevel_format = " %(levelname)s |"
98
98
99 def format(self, record):
99 def format(self, record):
100 if record.levelno >= self.highlevel_limit:
100 if record.levelno >= self.highlevel_limit:
101 record.highlevel = self.highlevel_format % record.__dict__
101 record.highlevel = self.highlevel_format % record.__dict__
102 else:
102 else:
103 record.highlevel = ""
103 record.highlevel = ""
104 return super(LevelFormatter, self).format(record)
104 return super(LevelFormatter, self).format(record)
105
105
106
106
107 class Application(SingletonConfigurable):
107 class Application(SingletonConfigurable):
108 """A singleton application with full configuration support."""
108 """A singleton application with full configuration support."""
109
109
110 # The name of the application, will usually match the name of the command
110 # The name of the application, will usually match the name of the command
111 # line application
111 # line application
112 name = Unicode(u'application')
112 name = Unicode(u'application')
113
113
114 # The description of the application that is printed at the beginning
114 # The description of the application that is printed at the beginning
115 # of the help.
115 # of the help.
116 description = Unicode(u'This is an application.')
116 description = Unicode(u'This is an application.')
117 # default section descriptions
117 # default section descriptions
118 option_description = Unicode(option_description)
118 option_description = Unicode(option_description)
119 keyvalue_description = Unicode(keyvalue_description)
119 keyvalue_description = Unicode(keyvalue_description)
120 subcommand_description = Unicode(subcommand_description)
120 subcommand_description = Unicode(subcommand_description)
121
121
122 python_config_loader_class = PyFileConfigLoader
122 python_config_loader_class = PyFileConfigLoader
123 json_config_loader_class = JSONFileConfigLoader
123 json_config_loader_class = JSONFileConfigLoader
124
124
125 # The usage and example string that goes at the end of the help string.
125 # The usage and example string that goes at the end of the help string.
126 examples = Unicode()
126 examples = Unicode()
127
127
128 # A sequence of Configurable subclasses whose config=True attributes will
128 # A sequence of Configurable subclasses whose config=True attributes will
129 # be exposed at the command line.
129 # be exposed at the command line.
130 classes = []
130 classes = []
131 @property
131 @property
132 def _help_classes(self):
132 def _help_classes(self):
133 """Define `App.help_classes` if CLI classes should differ from config file classes"""
133 """Define `App.help_classes` if CLI classes should differ from config file classes"""
134 return getattr(self, 'help_classes', self.classes)
134 return getattr(self, 'help_classes', self.classes)
135
135
136 @property
136 @property
137 def _config_classes(self):
137 def _config_classes(self):
138 """Define `App.config_classes` if config file classes should differ from CLI classes."""
138 """Define `App.config_classes` if config file classes should differ from CLI classes."""
139 return getattr(self, 'config_classes', self.classes)
139 return getattr(self, 'config_classes', self.classes)
140
140
141 # The version string of this application.
141 # The version string of this application.
142 version = Unicode(u'0.0')
142 version = Unicode(u'0.0')
143
143
144 # the argv used to initialize the application
144 # the argv used to initialize the application
145 argv = List()
145 argv = List()
146
146
147 # The log level for the application
147 # The log level for the application
148 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
148 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
149 default_value=logging.WARN,
149 default_value=logging.WARN,
150 config=True,
150 config=True,
151 help="Set the log level by value or name.")
151 help="Set the log level by value or name.")
152 def _log_level_changed(self, name, old, new):
152 def _log_level_changed(self, name, old, new):
153 """Adjust the log level when log_level is set."""
153 """Adjust the log level when log_level is set."""
154 if isinstance(new, string_types):
154 if isinstance(new, string_types):
155 new = getattr(logging, new)
155 new = getattr(logging, new)
156 self.log_level = new
156 self.log_level = new
157 self.log.setLevel(new)
157 self.log.setLevel(new)
158
158
159 _log_formatter_cls = LevelFormatter
159 _log_formatter_cls = LevelFormatter
160
160
161 log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", config=True,
161 log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", config=True,
162 help="The date format used by logging formatters for %(asctime)s"
162 help="The date format used by logging formatters for %(asctime)s"
163 )
163 )
164 def _log_datefmt_changed(self, name, old, new):
164 def _log_datefmt_changed(self, name, old, new):
165 self._log_format_changed('log_format', self.log_format, self.log_format)
165 self._log_format_changed('log_format', self.log_format, self.log_format)
166
166
167 log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True,
167 log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True,
168 help="The Logging format template",
168 help="The Logging format template",
169 )
169 )
170 def _log_format_changed(self, name, old, new):
170 def _log_format_changed(self, name, old, new):
171 """Change the log formatter when log_format is set."""
171 """Change the log formatter when log_format is set."""
172 _log_handler = self.log.handlers[0]
172 _log_handler = self.log.handlers[0]
173 _log_formatter = self._log_formatter_cls(fmt=new, datefmt=self.log_datefmt)
173 _log_formatter = self._log_formatter_cls(fmt=new, datefmt=self.log_datefmt)
174 _log_handler.setFormatter(_log_formatter)
174 _log_handler.setFormatter(_log_formatter)
175
175
176
176
177 log = Instance(logging.Logger)
177 log = Instance(logging.Logger)
178 def _log_default(self):
178 def _log_default(self):
179 """Start logging for this application.
179 """Start logging for this application.
180
180
181 The default is to log to stderr using a StreamHandler, if no default
181 The default is to log to stderr using a StreamHandler, if no default
182 handler already exists. The log level starts at logging.WARN, but this
182 handler already exists. The log level starts at logging.WARN, but this
183 can be adjusted by setting the ``log_level`` attribute.
183 can be adjusted by setting the ``log_level`` attribute.
184 """
184 """
185 log = logging.getLogger(self.__class__.__name__)
185 log = logging.getLogger(self.__class__.__name__)
186 log.setLevel(self.log_level)
186 log.setLevel(self.log_level)
187 log.propagate = False
187 log.propagate = False
188 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
188 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
189 while _log:
189 while _log:
190 if _log.handlers:
190 if _log.handlers:
191 return log
191 return log
192 if not _log.propagate:
192 if not _log.propagate:
193 break
193 break
194 else:
194 else:
195 _log = _log.parent
195 _log = _log.parent
196 if sys.executable.endswith('pythonw.exe'):
196 if sys.executable.endswith('pythonw.exe'):
197 # this should really go to a file, but file-logging is only
197 # this should really go to a file, but file-logging is only
198 # hooked up in parallel applications
198 # hooked up in parallel applications
199 _log_handler = logging.StreamHandler(open(os.devnull, 'w'))
199 _log_handler = logging.StreamHandler(open(os.devnull, 'w'))
200 else:
200 else:
201 _log_handler = logging.StreamHandler()
201 _log_handler = logging.StreamHandler()
202 _log_formatter = self._log_formatter_cls(fmt=self.log_format, datefmt=self.log_datefmt)
202 _log_formatter = self._log_formatter_cls(fmt=self.log_format, datefmt=self.log_datefmt)
203 _log_handler.setFormatter(_log_formatter)
203 _log_handler.setFormatter(_log_formatter)
204 log.addHandler(_log_handler)
204 log.addHandler(_log_handler)
205 return log
205 return log
206
206
207 # the alias map for configurables
207 # the alias map for configurables
208 aliases = Dict({'log-level' : 'Application.log_level'})
208 aliases = Dict({'log-level' : 'Application.log_level'})
209
209
210 # flags for loading Configurables or store_const style flags
210 # flags for loading Configurables or store_const style flags
211 # flags are loaded from this dict by '--key' flags
211 # flags are loaded from this dict by '--key' flags
212 # this must be a dict of two-tuples, the first element being the Config/dict
212 # this must be a dict of two-tuples, the first element being the Config/dict
213 # and the second being the help string for the flag
213 # and the second being the help string for the flag
214 flags = Dict()
214 flags = Dict()
215 def _flags_changed(self, name, old, new):
215 def _flags_changed(self, name, old, new):
216 """ensure flags dict is valid"""
216 """ensure flags dict is valid"""
217 for key,value in iteritems(new):
217 for key,value in iteritems(new):
218 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
218 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
219 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
219 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
220 assert isinstance(value[1], string_types), "Bad flag: %r:%s"%(key,value)
220 assert isinstance(value[1], string_types), "Bad flag: %r:%s"%(key,value)
221
221
222
222
223 # subcommands for launching other applications
223 # subcommands for launching other applications
224 # if this is not empty, this will be a parent Application
224 # if this is not empty, this will be a parent Application
225 # this must be a dict of two-tuples,
225 # this must be a dict of two-tuples,
226 # the first element being the application class/import string
226 # the first element being the application class/import string
227 # and the second being the help string for the subcommand
227 # and the second being the help string for the subcommand
228 subcommands = Dict()
228 subcommands = Dict()
229 # parse_command_line will initialize a subapp, if requested
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 # extra command-line arguments that don't set config values
232 # extra command-line arguments that don't set config values
233 extra_args = List(Unicode)
233 extra_args = List(Unicode)
234
234
235
235
236 def __init__(self, **kwargs):
236 def __init__(self, **kwargs):
237 SingletonConfigurable.__init__(self, **kwargs)
237 SingletonConfigurable.__init__(self, **kwargs)
238 # Ensure my class is in self.classes, so my attributes appear in command line
238 # Ensure my class is in self.classes, so my attributes appear in command line
239 # options and config files.
239 # options and config files.
240 if self.__class__ not in self.classes:
240 if self.__class__ not in self.classes:
241 self.classes.insert(0, self.__class__)
241 self.classes.insert(0, self.__class__)
242
242
243 def _config_changed(self, name, old, new):
243 def _config_changed(self, name, old, new):
244 SingletonConfigurable._config_changed(self, name, old, new)
244 SingletonConfigurable._config_changed(self, name, old, new)
245 self.log.debug('Config changed:')
245 self.log.debug('Config changed:')
246 self.log.debug(repr(new))
246 self.log.debug(repr(new))
247
247
248 @catch_config_error
248 @catch_config_error
249 def initialize(self, argv=None):
249 def initialize(self, argv=None):
250 """Do the basic steps to configure me.
250 """Do the basic steps to configure me.
251
251
252 Override in subclasses.
252 Override in subclasses.
253 """
253 """
254 self.parse_command_line(argv)
254 self.parse_command_line(argv)
255
255
256
256
257 def start(self):
257 def start(self):
258 """Start the app mainloop.
258 """Start the app mainloop.
259
259
260 Override in subclasses.
260 Override in subclasses.
261 """
261 """
262 if self.subapp is not None:
262 if self.subapp is not None:
263 return self.subapp.start()
263 return self.subapp.start()
264
264
265 def print_alias_help(self):
265 def print_alias_help(self):
266 """Print the alias part of the help."""
266 """Print the alias part of the help."""
267 if not self.aliases:
267 if not self.aliases:
268 return
268 return
269
269
270 lines = []
270 lines = []
271 classdict = {}
271 classdict = {}
272 for cls in self._help_classes:
272 for cls in self._help_classes:
273 # include all parents (up to, but excluding Configurable) in available names
273 # include all parents (up to, but excluding Configurable) in available names
274 for c in cls.mro()[:-3]:
274 for c in cls.mro()[:-3]:
275 classdict[c.__name__] = c
275 classdict[c.__name__] = c
276
276
277 for alias, longname in iteritems(self.aliases):
277 for alias, longname in iteritems(self.aliases):
278 classname, traitname = longname.split('.',1)
278 classname, traitname = longname.split('.',1)
279 cls = classdict[classname]
279 cls = classdict[classname]
280
280
281 trait = cls.class_traits(config=True)[traitname]
281 trait = cls.class_traits(config=True)[traitname]
282 help = cls.class_get_trait_help(trait).splitlines()
282 help = cls.class_get_trait_help(trait).splitlines()
283 # reformat first line
283 # reformat first line
284 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
284 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
285 if len(alias) == 1:
285 if len(alias) == 1:
286 help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
286 help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
287 lines.extend(help)
287 lines.extend(help)
288 # lines.append('')
288 # lines.append('')
289 print(os.linesep.join(lines))
289 print(os.linesep.join(lines))
290
290
291 def print_flag_help(self):
291 def print_flag_help(self):
292 """Print the flag part of the help."""
292 """Print the flag part of the help."""
293 if not self.flags:
293 if not self.flags:
294 return
294 return
295
295
296 lines = []
296 lines = []
297 for m, (cfg,help) in iteritems(self.flags):
297 for m, (cfg,help) in iteritems(self.flags):
298 prefix = '--' if len(m) > 1 else '-'
298 prefix = '--' if len(m) > 1 else '-'
299 lines.append(prefix+m)
299 lines.append(prefix+m)
300 lines.append(indent(dedent(help.strip())))
300 lines.append(indent(dedent(help.strip())))
301 # lines.append('')
301 # lines.append('')
302 print(os.linesep.join(lines))
302 print(os.linesep.join(lines))
303
303
304 def print_options(self):
304 def print_options(self):
305 if not self.flags and not self.aliases:
305 if not self.flags and not self.aliases:
306 return
306 return
307 lines = ['Options']
307 lines = ['Options']
308 lines.append('-'*len(lines[0]))
308 lines.append('-'*len(lines[0]))
309 lines.append('')
309 lines.append('')
310 for p in wrap_paragraphs(self.option_description):
310 for p in wrap_paragraphs(self.option_description):
311 lines.append(p)
311 lines.append(p)
312 lines.append('')
312 lines.append('')
313 print(os.linesep.join(lines))
313 print(os.linesep.join(lines))
314 self.print_flag_help()
314 self.print_flag_help()
315 self.print_alias_help()
315 self.print_alias_help()
316 print()
316 print()
317
317
318 def print_subcommands(self):
318 def print_subcommands(self):
319 """Print the subcommand part of the help."""
319 """Print the subcommand part of the help."""
320 if not self.subcommands:
320 if not self.subcommands:
321 return
321 return
322
322
323 lines = ["Subcommands"]
323 lines = ["Subcommands"]
324 lines.append('-'*len(lines[0]))
324 lines.append('-'*len(lines[0]))
325 lines.append('')
325 lines.append('')
326 for p in wrap_paragraphs(self.subcommand_description.format(
326 for p in wrap_paragraphs(self.subcommand_description.format(
327 app=self.name)):
327 app=self.name)):
328 lines.append(p)
328 lines.append(p)
329 lines.append('')
329 lines.append('')
330 for subc, (cls, help) in iteritems(self.subcommands):
330 for subc, (cls, help) in iteritems(self.subcommands):
331 lines.append(subc)
331 lines.append(subc)
332 if help:
332 if help:
333 lines.append(indent(dedent(help.strip())))
333 lines.append(indent(dedent(help.strip())))
334 lines.append('')
334 lines.append('')
335 print(os.linesep.join(lines))
335 print(os.linesep.join(lines))
336
336
337 def print_help(self, classes=False):
337 def print_help(self, classes=False):
338 """Print the help for each Configurable class in self.classes.
338 """Print the help for each Configurable class in self.classes.
339
339
340 If classes=False (the default), only flags and aliases are printed.
340 If classes=False (the default), only flags and aliases are printed.
341 """
341 """
342 self.print_description()
342 self.print_description()
343 self.print_subcommands()
343 self.print_subcommands()
344 self.print_options()
344 self.print_options()
345
345
346 if classes:
346 if classes:
347 help_classes = self._help_classes
347 help_classes = self._help_classes
348 if help_classes:
348 if help_classes:
349 print("Class parameters")
349 print("Class parameters")
350 print("----------------")
350 print("----------------")
351 print()
351 print()
352 for p in wrap_paragraphs(self.keyvalue_description):
352 for p in wrap_paragraphs(self.keyvalue_description):
353 print(p)
353 print(p)
354 print()
354 print()
355
355
356 for cls in help_classes:
356 for cls in help_classes:
357 cls.class_print_help()
357 cls.class_print_help()
358 print()
358 print()
359 else:
359 else:
360 print("To see all available configurables, use `--help-all`")
360 print("To see all available configurables, use `--help-all`")
361 print()
361 print()
362
362
363 self.print_examples()
363 self.print_examples()
364
364
365
365
366 def print_description(self):
366 def print_description(self):
367 """Print the application description."""
367 """Print the application description."""
368 for p in wrap_paragraphs(self.description):
368 for p in wrap_paragraphs(self.description):
369 print(p)
369 print(p)
370 print()
370 print()
371
371
372 def print_examples(self):
372 def print_examples(self):
373 """Print usage and examples.
373 """Print usage and examples.
374
374
375 This usage string goes at the end of the command line help string
375 This usage string goes at the end of the command line help string
376 and should contain examples of the application's usage.
376 and should contain examples of the application's usage.
377 """
377 """
378 if self.examples:
378 if self.examples:
379 print("Examples")
379 print("Examples")
380 print("--------")
380 print("--------")
381 print()
381 print()
382 print(indent(dedent(self.examples.strip())))
382 print(indent(dedent(self.examples.strip())))
383 print()
383 print()
384
384
385 def print_version(self):
385 def print_version(self):
386 """Print the version string."""
386 """Print the version string."""
387 print(self.version)
387 print(self.version)
388
388
389 def update_config(self, config):
389 def update_config(self, config):
390 """Fire the traits events when the config is updated."""
390 """Fire the traits events when the config is updated."""
391 # Save a copy of the current config.
391 # Save a copy of the current config.
392 newconfig = deepcopy(self.config)
392 newconfig = deepcopy(self.config)
393 # Merge the new config into the current one.
393 # Merge the new config into the current one.
394 newconfig.merge(config)
394 newconfig.merge(config)
395 # Save the combined config as self.config, which triggers the traits
395 # Save the combined config as self.config, which triggers the traits
396 # events.
396 # events.
397 self.config = newconfig
397 self.config = newconfig
398
398
399 @catch_config_error
399 @catch_config_error
400 def initialize_subcommand(self, subc, argv=None):
400 def initialize_subcommand(self, subc, argv=None):
401 """Initialize a subcommand with argv."""
401 """Initialize a subcommand with argv."""
402 subapp,help = self.subcommands.get(subc)
402 subapp,help = self.subcommands.get(subc)
403
403
404 if isinstance(subapp, string_types):
404 if isinstance(subapp, string_types):
405 subapp = import_item(subapp)
405 subapp = import_item(subapp)
406
406
407 # clear existing instances
407 # clear existing instances
408 self.__class__.clear_instance()
408 self.__class__.clear_instance()
409 # instantiate
409 # instantiate
410 self.subapp = subapp.instance(config=self.config)
410 self.subapp = subapp.instance(config=self.config)
411 # and initialize subapp
411 # and initialize subapp
412 self.subapp.initialize(argv)
412 self.subapp.initialize(argv)
413
413
414 def flatten_flags(self):
414 def flatten_flags(self):
415 """flatten flags and aliases, so cl-args override as expected.
415 """flatten flags and aliases, so cl-args override as expected.
416
416
417 This prevents issues such as an alias pointing to InteractiveShell,
417 This prevents issues such as an alias pointing to InteractiveShell,
418 but a config file setting the same trait in TerminalInteraciveShell
418 but a config file setting the same trait in TerminalInteraciveShell
419 getting inappropriate priority over the command-line arg.
419 getting inappropriate priority over the command-line arg.
420
420
421 Only aliases with exactly one descendent in the class list
421 Only aliases with exactly one descendent in the class list
422 will be promoted.
422 will be promoted.
423
423
424 """
424 """
425 # build a tree of classes in our list that inherit from a particular
425 # build a tree of classes in our list that inherit from a particular
426 # it will be a dict by parent classname of classes in our list
426 # it will be a dict by parent classname of classes in our list
427 # that are descendents
427 # that are descendents
428 mro_tree = defaultdict(list)
428 mro_tree = defaultdict(list)
429 for cls in self._help_classes:
429 for cls in self._help_classes:
430 clsname = cls.__name__
430 clsname = cls.__name__
431 for parent in cls.mro()[1:-3]:
431 for parent in cls.mro()[1:-3]:
432 # exclude cls itself and Configurable,HasTraits,object
432 # exclude cls itself and Configurable,HasTraits,object
433 mro_tree[parent.__name__].append(clsname)
433 mro_tree[parent.__name__].append(clsname)
434 # flatten aliases, which have the form:
434 # flatten aliases, which have the form:
435 # { 'alias' : 'Class.trait' }
435 # { 'alias' : 'Class.trait' }
436 aliases = {}
436 aliases = {}
437 for alias, cls_trait in iteritems(self.aliases):
437 for alias, cls_trait in iteritems(self.aliases):
438 cls,trait = cls_trait.split('.',1)
438 cls,trait = cls_trait.split('.',1)
439 children = mro_tree[cls]
439 children = mro_tree[cls]
440 if len(children) == 1:
440 if len(children) == 1:
441 # exactly one descendent, promote alias
441 # exactly one descendent, promote alias
442 cls = children[0]
442 cls = children[0]
443 aliases[alias] = '.'.join([cls,trait])
443 aliases[alias] = '.'.join([cls,trait])
444
444
445 # flatten flags, which are of the form:
445 # flatten flags, which are of the form:
446 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
446 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
447 flags = {}
447 flags = {}
448 for key, (flagdict, help) in iteritems(self.flags):
448 for key, (flagdict, help) in iteritems(self.flags):
449 newflag = {}
449 newflag = {}
450 for cls, subdict in iteritems(flagdict):
450 for cls, subdict in iteritems(flagdict):
451 children = mro_tree[cls]
451 children = mro_tree[cls]
452 # exactly one descendent, promote flag section
452 # exactly one descendent, promote flag section
453 if len(children) == 1:
453 if len(children) == 1:
454 cls = children[0]
454 cls = children[0]
455 newflag[cls] = subdict
455 newflag[cls] = subdict
456 flags[key] = (newflag, help)
456 flags[key] = (newflag, help)
457 return flags, aliases
457 return flags, aliases
458
458
459 @catch_config_error
459 @catch_config_error
460 def parse_command_line(self, argv=None):
460 def parse_command_line(self, argv=None):
461 """Parse the command line arguments."""
461 """Parse the command line arguments."""
462 argv = sys.argv[1:] if argv is None else argv
462 argv = sys.argv[1:] if argv is None else argv
463 self.argv = [ py3compat.cast_unicode(arg) for arg in argv ]
463 self.argv = [ py3compat.cast_unicode(arg) for arg in argv ]
464
464
465 if argv and argv[0] == 'help':
465 if argv and argv[0] == 'help':
466 # turn `ipython help notebook` into `ipython notebook -h`
466 # turn `ipython help notebook` into `ipython notebook -h`
467 argv = argv[1:] + ['-h']
467 argv = argv[1:] + ['-h']
468
468
469 if self.subcommands and len(argv) > 0:
469 if self.subcommands and len(argv) > 0:
470 # we have subcommands, and one may have been specified
470 # we have subcommands, and one may have been specified
471 subc, subargv = argv[0], argv[1:]
471 subc, subargv = argv[0], argv[1:]
472 if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands:
472 if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands:
473 # it's a subcommand, and *not* a flag or class parameter
473 # it's a subcommand, and *not* a flag or class parameter
474 return self.initialize_subcommand(subc, subargv)
474 return self.initialize_subcommand(subc, subargv)
475
475
476 # Arguments after a '--' argument are for the script IPython may be
476 # Arguments after a '--' argument are for the script IPython may be
477 # about to run, not IPython iteslf. For arguments parsed here (help and
477 # about to run, not IPython iteslf. For arguments parsed here (help and
478 # version), we want to only search the arguments up to the first
478 # version), we want to only search the arguments up to the first
479 # occurrence of '--', which we're calling interpreted_argv.
479 # occurrence of '--', which we're calling interpreted_argv.
480 try:
480 try:
481 interpreted_argv = argv[:argv.index('--')]
481 interpreted_argv = argv[:argv.index('--')]
482 except ValueError:
482 except ValueError:
483 interpreted_argv = argv
483 interpreted_argv = argv
484
484
485 if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')):
485 if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')):
486 self.print_help('--help-all' in interpreted_argv)
486 self.print_help('--help-all' in interpreted_argv)
487 self.exit(0)
487 self.exit(0)
488
488
489 if '--version' in interpreted_argv or '-V' in interpreted_argv:
489 if '--version' in interpreted_argv or '-V' in interpreted_argv:
490 self.print_version()
490 self.print_version()
491 self.exit(0)
491 self.exit(0)
492
492
493 # flatten flags&aliases, so cl-args get appropriate priority:
493 # flatten flags&aliases, so cl-args get appropriate priority:
494 flags,aliases = self.flatten_flags()
494 flags,aliases = self.flatten_flags()
495 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
495 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
496 flags=flags, log=self.log)
496 flags=flags, log=self.log)
497 config = loader.load_config()
497 config = loader.load_config()
498 self.update_config(config)
498 self.update_config(config)
499 # store unparsed args in extra_args
499 # store unparsed args in extra_args
500 self.extra_args = loader.extra_args
500 self.extra_args = loader.extra_args
501
501
502 @classmethod
502 @classmethod
503 def _load_config_files(cls, basefilename, path=None, log=None):
503 def _load_config_files(cls, basefilename, path=None, log=None):
504 """Load config files (py,json) by filename and path.
504 """Load config files (py,json) by filename and path.
505
505
506 yield each config object in turn.
506 yield each config object in turn.
507 """
507 """
508
508
509 if not isinstance(path, list):
509 if not isinstance(path, list):
510 path = [path]
510 path = [path]
511 for path in path[::-1]:
511 for path in path[::-1]:
512 # path list is in descending priority order, so load files backwards:
512 # path list is in descending priority order, so load files backwards:
513 pyloader = cls.python_config_loader_class(basefilename+'.py', path=path, log=log)
513 pyloader = cls.python_config_loader_class(basefilename+'.py', path=path, log=log)
514 jsonloader = cls.json_config_loader_class(basefilename+'.json', path=path, log=log)
514 jsonloader = cls.json_config_loader_class(basefilename+'.json', path=path, log=log)
515 config = None
515 config = None
516 for loader in [pyloader, jsonloader]:
516 for loader in [pyloader, jsonloader]:
517 try:
517 try:
518 config = loader.load_config()
518 config = loader.load_config()
519 except ConfigFileNotFound:
519 except ConfigFileNotFound:
520 pass
520 pass
521 except Exception:
521 except Exception:
522 # try to get the full filename, but it will be empty in the
522 # try to get the full filename, but it will be empty in the
523 # unlikely event that the error raised before filefind finished
523 # unlikely event that the error raised before filefind finished
524 filename = loader.full_filename or basefilename
524 filename = loader.full_filename or basefilename
525 # problem while running the file
525 # problem while running the file
526 if log:
526 if log:
527 log.error("Exception while loading config file %s",
527 log.error("Exception while loading config file %s",
528 filename, exc_info=True)
528 filename, exc_info=True)
529 else:
529 else:
530 if log:
530 if log:
531 log.debug("Loaded config file: %s", loader.full_filename)
531 log.debug("Loaded config file: %s", loader.full_filename)
532 if config:
532 if config:
533 yield config
533 yield config
534
534
535 raise StopIteration
535 raise StopIteration
536
536
537
537
538 @catch_config_error
538 @catch_config_error
539 def load_config_file(self, filename, path=None):
539 def load_config_file(self, filename, path=None):
540 """Load config files by filename and path."""
540 """Load config files by filename and path."""
541 filename, ext = os.path.splitext(filename)
541 filename, ext = os.path.splitext(filename)
542 loaded = []
542 loaded = []
543 for config in self._load_config_files(filename, path=path, log=self.log):
543 for config in self._load_config_files(filename, path=path, log=self.log):
544 loaded.append(config)
544 loaded.append(config)
545 self.update_config(config)
545 self.update_config(config)
546 if len(loaded) > 1:
546 if len(loaded) > 1:
547 collisions = loaded[0].collisions(loaded[1])
547 collisions = loaded[0].collisions(loaded[1])
548 if collisions:
548 if collisions:
549 self.log.warn("Collisions detected in {0}.py and {0}.json config files."
549 self.log.warn("Collisions detected in {0}.py and {0}.json config files."
550 " {0}.json has higher priority: {1}".format(
550 " {0}.json has higher priority: {1}".format(
551 filename, json.dumps(collisions, indent=2),
551 filename, json.dumps(collisions, indent=2),
552 ))
552 ))
553
553
554
554
555 def generate_config_file(self):
555 def generate_config_file(self):
556 """generate default config file from Configurables"""
556 """generate default config file from Configurables"""
557 lines = ["# Configuration file for %s." % self.name]
557 lines = ["# Configuration file for %s." % self.name]
558 lines.append('')
558 lines.append('')
559 for cls in self._config_classes:
559 for cls in self._config_classes:
560 lines.append(cls.class_config_section())
560 lines.append(cls.class_config_section())
561 return '\n'.join(lines)
561 return '\n'.join(lines)
562
562
563 def exit(self, exit_status=0):
563 def exit(self, exit_status=0):
564 self.log.debug("Exiting application: %s" % self.name)
564 self.log.debug("Exiting application: %s" % self.name)
565 sys.exit(exit_status)
565 sys.exit(exit_status)
566
566
567 @classmethod
567 @classmethod
568 def launch_instance(cls, argv=None, **kwargs):
568 def launch_instance(cls, argv=None, **kwargs):
569 """Launch a global instance of this Application
569 """Launch a global instance of this Application
570
570
571 If a global instance already exists, this reinitializes and starts it
571 If a global instance already exists, this reinitializes and starts it
572 """
572 """
573 app = cls.instance(**kwargs)
573 app = cls.instance(**kwargs)
574 app.initialize(argv)
574 app.initialize(argv)
575 app.start()
575 app.start()
576
576
577 #-----------------------------------------------------------------------------
577 #-----------------------------------------------------------------------------
578 # utility functions, for convenience
578 # utility functions, for convenience
579 #-----------------------------------------------------------------------------
579 #-----------------------------------------------------------------------------
580
580
581 def boolean_flag(name, configurable, set_help='', unset_help=''):
581 def boolean_flag(name, configurable, set_help='', unset_help=''):
582 """Helper for building basic --trait, --no-trait flags.
582 """Helper for building basic --trait, --no-trait flags.
583
583
584 Parameters
584 Parameters
585 ----------
585 ----------
586
586
587 name : str
587 name : str
588 The name of the flag.
588 The name of the flag.
589 configurable : str
589 configurable : str
590 The 'Class.trait' string of the trait to be set/unset with the flag
590 The 'Class.trait' string of the trait to be set/unset with the flag
591 set_help : unicode
591 set_help : unicode
592 help string for --name flag
592 help string for --name flag
593 unset_help : unicode
593 unset_help : unicode
594 help string for --no-name flag
594 help string for --no-name flag
595
595
596 Returns
596 Returns
597 -------
597 -------
598
598
599 cfg : dict
599 cfg : dict
600 A dict with two keys: 'name', and 'no-name', for setting and unsetting
600 A dict with two keys: 'name', and 'no-name', for setting and unsetting
601 the trait, respectively.
601 the trait, respectively.
602 """
602 """
603 # default helpstrings
603 # default helpstrings
604 set_help = set_help or "set %s=True"%configurable
604 set_help = set_help or "set %s=True"%configurable
605 unset_help = unset_help or "set %s=False"%configurable
605 unset_help = unset_help or "set %s=False"%configurable
606
606
607 cls,trait = configurable.split('.')
607 cls,trait = configurable.split('.')
608
608
609 setter = {cls : {trait : True}}
609 setter = {cls : {trait : True}}
610 unsetter = {cls : {trait : False}}
610 unsetter = {cls : {trait : False}}
611 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
611 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
612
612
613
613
614 def get_config():
614 def get_config():
615 """Get the config object for the global Application instance, if there is one
615 """Get the config object for the global Application instance, if there is one
616
616
617 otherwise return an empty config object
617 otherwise return an empty config object
618 """
618 """
619 if Application.initialized():
619 if Application.initialized():
620 return Application.instance().config
620 return Application.instance().config
621 else:
621 else:
622 return Config()
622 return Config()
@@ -1,380 +1,380 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """A base class for objects that are configurable."""
2 """A base class for objects that are configurable."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import logging
9 import logging
10 from copy import deepcopy
10 from copy import deepcopy
11
11
12 from .loader import Config, LazyConfigValue
12 from .loader import Config, LazyConfigValue
13 from IPython.utils.traitlets import HasTraits, Instance
13 from traitlets.traitlets import HasTraits, Instance
14 from IPython.utils.text import indent, wrap_paragraphs
14 from IPython.utils.text import indent, wrap_paragraphs
15 from IPython.utils.py3compat import iteritems
15 from IPython.utils.py3compat import iteritems
16
16
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Helper classes for Configurables
19 # Helper classes for Configurables
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22
22
23 class ConfigurableError(Exception):
23 class ConfigurableError(Exception):
24 pass
24 pass
25
25
26
26
27 class MultipleInstanceError(ConfigurableError):
27 class MultipleInstanceError(ConfigurableError):
28 pass
28 pass
29
29
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 # Configurable implementation
31 # Configurable implementation
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33
33
34 class Configurable(HasTraits):
34 class Configurable(HasTraits):
35
35
36 config = Instance(Config, (), {})
36 config = Instance(Config, (), {})
37 parent = Instance('IPython.config.configurable.Configurable', allow_none=True)
37 parent = Instance('traitlets.config.configurable.Configurable', allow_none=True)
38
38
39 def __init__(self, **kwargs):
39 def __init__(self, **kwargs):
40 """Create a configurable given a config config.
40 """Create a configurable given a config config.
41
41
42 Parameters
42 Parameters
43 ----------
43 ----------
44 config : Config
44 config : Config
45 If this is empty, default values are used. If config is a
45 If this is empty, default values are used. If config is a
46 :class:`Config` instance, it will be used to configure the
46 :class:`Config` instance, it will be used to configure the
47 instance.
47 instance.
48 parent : Configurable instance, optional
48 parent : Configurable instance, optional
49 The parent Configurable instance of this object.
49 The parent Configurable instance of this object.
50
50
51 Notes
51 Notes
52 -----
52 -----
53 Subclasses of Configurable must call the :meth:`__init__` method of
53 Subclasses of Configurable must call the :meth:`__init__` method of
54 :class:`Configurable` *before* doing anything else and using
54 :class:`Configurable` *before* doing anything else and using
55 :func:`super`::
55 :func:`super`::
56
56
57 class MyConfigurable(Configurable):
57 class MyConfigurable(Configurable):
58 def __init__(self, config=None):
58 def __init__(self, config=None):
59 super(MyConfigurable, self).__init__(config=config)
59 super(MyConfigurable, self).__init__(config=config)
60 # Then any other code you need to finish initialization.
60 # Then any other code you need to finish initialization.
61
61
62 This ensures that instances will be configured properly.
62 This ensures that instances will be configured properly.
63 """
63 """
64 parent = kwargs.pop('parent', None)
64 parent = kwargs.pop('parent', None)
65 if parent is not None:
65 if parent is not None:
66 # config is implied from parent
66 # config is implied from parent
67 if kwargs.get('config', None) is None:
67 if kwargs.get('config', None) is None:
68 kwargs['config'] = parent.config
68 kwargs['config'] = parent.config
69 self.parent = parent
69 self.parent = parent
70
70
71 config = kwargs.pop('config', None)
71 config = kwargs.pop('config', None)
72
72
73 # load kwarg traits, other than config
73 # load kwarg traits, other than config
74 super(Configurable, self).__init__(**kwargs)
74 super(Configurable, self).__init__(**kwargs)
75
75
76 # load config
76 # load config
77 if config is not None:
77 if config is not None:
78 # We used to deepcopy, but for now we are trying to just save
78 # We used to deepcopy, but for now we are trying to just save
79 # by reference. This *could* have side effects as all components
79 # by reference. This *could* have side effects as all components
80 # will share config. In fact, I did find such a side effect in
80 # will share config. In fact, I did find such a side effect in
81 # _config_changed below. If a config attribute value was a mutable type
81 # _config_changed below. If a config attribute value was a mutable type
82 # all instances of a component were getting the same copy, effectively
82 # all instances of a component were getting the same copy, effectively
83 # making that a class attribute.
83 # making that a class attribute.
84 # self.config = deepcopy(config)
84 # self.config = deepcopy(config)
85 self.config = config
85 self.config = config
86 else:
86 else:
87 # allow _config_default to return something
87 # allow _config_default to return something
88 self._load_config(self.config)
88 self._load_config(self.config)
89
89
90 # Ensure explicit kwargs are applied after loading config.
90 # Ensure explicit kwargs are applied after loading config.
91 # This is usually redundant, but ensures config doesn't override
91 # This is usually redundant, but ensures config doesn't override
92 # explicitly assigned values.
92 # explicitly assigned values.
93 for key, value in kwargs.items():
93 for key, value in kwargs.items():
94 setattr(self, key, value)
94 setattr(self, key, value)
95
95
96 #-------------------------------------------------------------------------
96 #-------------------------------------------------------------------------
97 # Static trait notifiations
97 # Static trait notifiations
98 #-------------------------------------------------------------------------
98 #-------------------------------------------------------------------------
99
99
100 @classmethod
100 @classmethod
101 def section_names(cls):
101 def section_names(cls):
102 """return section names as a list"""
102 """return section names as a list"""
103 return [c.__name__ for c in reversed(cls.__mro__) if
103 return [c.__name__ for c in reversed(cls.__mro__) if
104 issubclass(c, Configurable) and issubclass(cls, c)
104 issubclass(c, Configurable) and issubclass(cls, c)
105 ]
105 ]
106
106
107 def _find_my_config(self, cfg):
107 def _find_my_config(self, cfg):
108 """extract my config from a global Config object
108 """extract my config from a global Config object
109
109
110 will construct a Config object of only the config values that apply to me
110 will construct a Config object of only the config values that apply to me
111 based on my mro(), as well as those of my parent(s) if they exist.
111 based on my mro(), as well as those of my parent(s) if they exist.
112
112
113 If I am Bar and my parent is Foo, and their parent is Tim,
113 If I am Bar and my parent is Foo, and their parent is Tim,
114 this will return merge following config sections, in this order::
114 this will return merge following config sections, in this order::
115
115
116 [Bar, Foo.bar, Tim.Foo.Bar]
116 [Bar, Foo.bar, Tim.Foo.Bar]
117
117
118 With the last item being the highest priority.
118 With the last item being the highest priority.
119 """
119 """
120 cfgs = [cfg]
120 cfgs = [cfg]
121 if self.parent:
121 if self.parent:
122 cfgs.append(self.parent._find_my_config(cfg))
122 cfgs.append(self.parent._find_my_config(cfg))
123 my_config = Config()
123 my_config = Config()
124 for c in cfgs:
124 for c in cfgs:
125 for sname in self.section_names():
125 for sname in self.section_names():
126 # Don't do a blind getattr as that would cause the config to
126 # Don't do a blind getattr as that would cause the config to
127 # dynamically create the section with name Class.__name__.
127 # dynamically create the section with name Class.__name__.
128 if c._has_section(sname):
128 if c._has_section(sname):
129 my_config.merge(c[sname])
129 my_config.merge(c[sname])
130 return my_config
130 return my_config
131
131
132 def _load_config(self, cfg, section_names=None, traits=None):
132 def _load_config(self, cfg, section_names=None, traits=None):
133 """load traits from a Config object"""
133 """load traits from a Config object"""
134
134
135 if traits is None:
135 if traits is None:
136 traits = self.traits(config=True)
136 traits = self.traits(config=True)
137 if section_names is None:
137 if section_names is None:
138 section_names = self.section_names()
138 section_names = self.section_names()
139
139
140 my_config = self._find_my_config(cfg)
140 my_config = self._find_my_config(cfg)
141
141
142 # hold trait notifications until after all config has been loaded
142 # hold trait notifications until after all config has been loaded
143 with self.hold_trait_notifications():
143 with self.hold_trait_notifications():
144 for name, config_value in iteritems(my_config):
144 for name, config_value in iteritems(my_config):
145 if name in traits:
145 if name in traits:
146 if isinstance(config_value, LazyConfigValue):
146 if isinstance(config_value, LazyConfigValue):
147 # ConfigValue is a wrapper for using append / update on containers
147 # ConfigValue is a wrapper for using append / update on containers
148 # without having to copy the initial value
148 # without having to copy the initial value
149 initial = getattr(self, name)
149 initial = getattr(self, name)
150 config_value = config_value.get_value(initial)
150 config_value = config_value.get_value(initial)
151 # We have to do a deepcopy here if we don't deepcopy the entire
151 # We have to do a deepcopy here if we don't deepcopy the entire
152 # config object. If we don't, a mutable config_value will be
152 # config object. If we don't, a mutable config_value will be
153 # shared by all instances, effectively making it a class attribute.
153 # shared by all instances, effectively making it a class attribute.
154 setattr(self, name, deepcopy(config_value))
154 setattr(self, name, deepcopy(config_value))
155
155
156 def _config_changed(self, name, old, new):
156 def _config_changed(self, name, old, new):
157 """Update all the class traits having ``config=True`` as metadata.
157 """Update all the class traits having ``config=True`` as metadata.
158
158
159 For any class trait with a ``config`` metadata attribute that is
159 For any class trait with a ``config`` metadata attribute that is
160 ``True``, we update the trait with the value of the corresponding
160 ``True``, we update the trait with the value of the corresponding
161 config entry.
161 config entry.
162 """
162 """
163 # Get all traits with a config metadata entry that is True
163 # Get all traits with a config metadata entry that is True
164 traits = self.traits(config=True)
164 traits = self.traits(config=True)
165
165
166 # We auto-load config section for this class as well as any parent
166 # We auto-load config section for this class as well as any parent
167 # classes that are Configurable subclasses. This starts with Configurable
167 # classes that are Configurable subclasses. This starts with Configurable
168 # and works down the mro loading the config for each section.
168 # and works down the mro loading the config for each section.
169 section_names = self.section_names()
169 section_names = self.section_names()
170 self._load_config(new, traits=traits, section_names=section_names)
170 self._load_config(new, traits=traits, section_names=section_names)
171
171
172 def update_config(self, config):
172 def update_config(self, config):
173 """Fire the traits events when the config is updated."""
173 """Fire the traits events when the config is updated."""
174 # Save a copy of the current config.
174 # Save a copy of the current config.
175 newconfig = deepcopy(self.config)
175 newconfig = deepcopy(self.config)
176 # Merge the new config into the current one.
176 # Merge the new config into the current one.
177 newconfig.merge(config)
177 newconfig.merge(config)
178 # Save the combined config as self.config, which triggers the traits
178 # Save the combined config as self.config, which triggers the traits
179 # events.
179 # events.
180 self.config = newconfig
180 self.config = newconfig
181
181
182 @classmethod
182 @classmethod
183 def class_get_help(cls, inst=None):
183 def class_get_help(cls, inst=None):
184 """Get the help string for this class in ReST format.
184 """Get the help string for this class in ReST format.
185
185
186 If `inst` is given, it's current trait values will be used in place of
186 If `inst` is given, it's current trait values will be used in place of
187 class defaults.
187 class defaults.
188 """
188 """
189 assert inst is None or isinstance(inst, cls)
189 assert inst is None or isinstance(inst, cls)
190 final_help = []
190 final_help = []
191 final_help.append(u'%s options' % cls.__name__)
191 final_help.append(u'%s options' % cls.__name__)
192 final_help.append(len(final_help[0])*u'-')
192 final_help.append(len(final_help[0])*u'-')
193 for k, v in sorted(cls.class_traits(config=True).items()):
193 for k, v in sorted(cls.class_traits(config=True).items()):
194 help = cls.class_get_trait_help(v, inst)
194 help = cls.class_get_trait_help(v, inst)
195 final_help.append(help)
195 final_help.append(help)
196 return '\n'.join(final_help)
196 return '\n'.join(final_help)
197
197
198 @classmethod
198 @classmethod
199 def class_get_trait_help(cls, trait, inst=None):
199 def class_get_trait_help(cls, trait, inst=None):
200 """Get the help string for a single trait.
200 """Get the help string for a single trait.
201
201
202 If `inst` is given, it's current trait values will be used in place of
202 If `inst` is given, it's current trait values will be used in place of
203 the class default.
203 the class default.
204 """
204 """
205 assert inst is None or isinstance(inst, cls)
205 assert inst is None or isinstance(inst, cls)
206 lines = []
206 lines = []
207 header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
207 header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
208 lines.append(header)
208 lines.append(header)
209 if inst is not None:
209 if inst is not None:
210 lines.append(indent('Current: %r' % getattr(inst, trait.name), 4))
210 lines.append(indent('Current: %r' % getattr(inst, trait.name), 4))
211 else:
211 else:
212 try:
212 try:
213 dvr = repr(trait.get_default_value())
213 dvr = repr(trait.get_default_value())
214 except Exception:
214 except Exception:
215 dvr = None # ignore defaults we can't construct
215 dvr = None # ignore defaults we can't construct
216 if dvr is not None:
216 if dvr is not None:
217 if len(dvr) > 64:
217 if len(dvr) > 64:
218 dvr = dvr[:61]+'...'
218 dvr = dvr[:61]+'...'
219 lines.append(indent('Default: %s' % dvr, 4))
219 lines.append(indent('Default: %s' % dvr, 4))
220 if 'Enum' in trait.__class__.__name__:
220 if 'Enum' in trait.__class__.__name__:
221 # include Enum choices
221 # include Enum choices
222 lines.append(indent('Choices: %r' % (trait.values,)))
222 lines.append(indent('Choices: %r' % (trait.values,)))
223
223
224 help = trait.get_metadata('help')
224 help = trait.get_metadata('help')
225 if help is not None:
225 if help is not None:
226 help = '\n'.join(wrap_paragraphs(help, 76))
226 help = '\n'.join(wrap_paragraphs(help, 76))
227 lines.append(indent(help, 4))
227 lines.append(indent(help, 4))
228 return '\n'.join(lines)
228 return '\n'.join(lines)
229
229
230 @classmethod
230 @classmethod
231 def class_print_help(cls, inst=None):
231 def class_print_help(cls, inst=None):
232 """Get the help string for a single trait and print it."""
232 """Get the help string for a single trait and print it."""
233 print(cls.class_get_help(inst))
233 print(cls.class_get_help(inst))
234
234
235 @classmethod
235 @classmethod
236 def class_config_section(cls):
236 def class_config_section(cls):
237 """Get the config class config section"""
237 """Get the config class config section"""
238 def c(s):
238 def c(s):
239 """return a commented, wrapped block."""
239 """return a commented, wrapped block."""
240 s = '\n\n'.join(wrap_paragraphs(s, 78))
240 s = '\n\n'.join(wrap_paragraphs(s, 78))
241
241
242 return '# ' + s.replace('\n', '\n# ')
242 return '# ' + s.replace('\n', '\n# ')
243
243
244 # section header
244 # section header
245 breaker = '#' + '-'*78
245 breaker = '#' + '-'*78
246 s = "# %s configuration" % cls.__name__
246 s = "# %s configuration" % cls.__name__
247 lines = [breaker, s, breaker, '']
247 lines = [breaker, s, breaker, '']
248 # get the description trait
248 # get the description trait
249 desc = cls.class_traits().get('description')
249 desc = cls.class_traits().get('description')
250 if desc:
250 if desc:
251 desc = desc.default_value
251 desc = desc.default_value
252 else:
252 else:
253 # no description trait, use __doc__
253 # no description trait, use __doc__
254 desc = getattr(cls, '__doc__', '')
254 desc = getattr(cls, '__doc__', '')
255 if desc:
255 if desc:
256 lines.append(c(desc))
256 lines.append(c(desc))
257 lines.append('')
257 lines.append('')
258
258
259 parents = []
259 parents = []
260 for parent in cls.mro():
260 for parent in cls.mro():
261 # only include parents that are not base classes
261 # only include parents that are not base classes
262 # and are not the class itself
262 # and are not the class itself
263 # and have some configurable traits to inherit
263 # and have some configurable traits to inherit
264 if parent is not cls and issubclass(parent, Configurable) and \
264 if parent is not cls and issubclass(parent, Configurable) and \
265 parent.class_traits(config=True):
265 parent.class_traits(config=True):
266 parents.append(parent)
266 parents.append(parent)
267
267
268 if parents:
268 if parents:
269 pstr = ', '.join([ p.__name__ for p in parents ])
269 pstr = ', '.join([ p.__name__ for p in parents ])
270 lines.append(c('%s will inherit config from: %s'%(cls.__name__, pstr)))
270 lines.append(c('%s will inherit config from: %s'%(cls.__name__, pstr)))
271 lines.append('')
271 lines.append('')
272
272
273 for name, trait in iteritems(cls.class_traits(config=True)):
273 for name, trait in iteritems(cls.class_traits(config=True)):
274 help = trait.get_metadata('help') or ''
274 help = trait.get_metadata('help') or ''
275 lines.append(c(help))
275 lines.append(c(help))
276 lines.append('# c.%s.%s = %r'%(cls.__name__, name, trait.get_default_value()))
276 lines.append('# c.%s.%s = %r'%(cls.__name__, name, trait.get_default_value()))
277 lines.append('')
277 lines.append('')
278 return '\n'.join(lines)
278 return '\n'.join(lines)
279
279
280
280
281
281
282 class SingletonConfigurable(Configurable):
282 class SingletonConfigurable(Configurable):
283 """A configurable that only allows one instance.
283 """A configurable that only allows one instance.
284
284
285 This class is for classes that should only have one instance of itself
285 This class is for classes that should only have one instance of itself
286 or *any* subclass. To create and retrieve such a class use the
286 or *any* subclass. To create and retrieve such a class use the
287 :meth:`SingletonConfigurable.instance` method.
287 :meth:`SingletonConfigurable.instance` method.
288 """
288 """
289
289
290 _instance = None
290 _instance = None
291
291
292 @classmethod
292 @classmethod
293 def _walk_mro(cls):
293 def _walk_mro(cls):
294 """Walk the cls.mro() for parent classes that are also singletons
294 """Walk the cls.mro() for parent classes that are also singletons
295
295
296 For use in instance()
296 For use in instance()
297 """
297 """
298
298
299 for subclass in cls.mro():
299 for subclass in cls.mro():
300 if issubclass(cls, subclass) and \
300 if issubclass(cls, subclass) and \
301 issubclass(subclass, SingletonConfigurable) and \
301 issubclass(subclass, SingletonConfigurable) and \
302 subclass != SingletonConfigurable:
302 subclass != SingletonConfigurable:
303 yield subclass
303 yield subclass
304
304
305 @classmethod
305 @classmethod
306 def clear_instance(cls):
306 def clear_instance(cls):
307 """unset _instance for this class and singleton parents.
307 """unset _instance for this class and singleton parents.
308 """
308 """
309 if not cls.initialized():
309 if not cls.initialized():
310 return
310 return
311 for subclass in cls._walk_mro():
311 for subclass in cls._walk_mro():
312 if isinstance(subclass._instance, cls):
312 if isinstance(subclass._instance, cls):
313 # only clear instances that are instances
313 # only clear instances that are instances
314 # of the calling class
314 # of the calling class
315 subclass._instance = None
315 subclass._instance = None
316
316
317 @classmethod
317 @classmethod
318 def instance(cls, *args, **kwargs):
318 def instance(cls, *args, **kwargs):
319 """Returns a global instance of this class.
319 """Returns a global instance of this class.
320
320
321 This method create a new instance if none have previously been created
321 This method create a new instance if none have previously been created
322 and returns a previously created instance is one already exists.
322 and returns a previously created instance is one already exists.
323
323
324 The arguments and keyword arguments passed to this method are passed
324 The arguments and keyword arguments passed to this method are passed
325 on to the :meth:`__init__` method of the class upon instantiation.
325 on to the :meth:`__init__` method of the class upon instantiation.
326
326
327 Examples
327 Examples
328 --------
328 --------
329
329
330 Create a singleton class using instance, and retrieve it::
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 >>> class Foo(SingletonConfigurable): pass
333 >>> class Foo(SingletonConfigurable): pass
334 >>> foo = Foo.instance()
334 >>> foo = Foo.instance()
335 >>> foo == Foo.instance()
335 >>> foo == Foo.instance()
336 True
336 True
337
337
338 Create a subclass that is retrived using the base class instance::
338 Create a subclass that is retrived using the base class instance::
339
339
340 >>> class Bar(SingletonConfigurable): pass
340 >>> class Bar(SingletonConfigurable): pass
341 >>> class Bam(Bar): pass
341 >>> class Bam(Bar): pass
342 >>> bam = Bam.instance()
342 >>> bam = Bam.instance()
343 >>> bam == Bar.instance()
343 >>> bam == Bar.instance()
344 True
344 True
345 """
345 """
346 # Create and save the instance
346 # Create and save the instance
347 if cls._instance is None:
347 if cls._instance is None:
348 inst = cls(*args, **kwargs)
348 inst = cls(*args, **kwargs)
349 # Now make sure that the instance will also be returned by
349 # Now make sure that the instance will also be returned by
350 # parent classes' _instance attribute.
350 # parent classes' _instance attribute.
351 for subclass in cls._walk_mro():
351 for subclass in cls._walk_mro():
352 subclass._instance = inst
352 subclass._instance = inst
353
353
354 if isinstance(cls._instance, cls):
354 if isinstance(cls._instance, cls):
355 return cls._instance
355 return cls._instance
356 else:
356 else:
357 raise MultipleInstanceError(
357 raise MultipleInstanceError(
358 'Multiple incompatible subclass instances of '
358 'Multiple incompatible subclass instances of '
359 '%s are being created.' % cls.__name__
359 '%s are being created.' % cls.__name__
360 )
360 )
361
361
362 @classmethod
362 @classmethod
363 def initialized(cls):
363 def initialized(cls):
364 """Has an instance been created?"""
364 """Has an instance been created?"""
365 return hasattr(cls, "_instance") and cls._instance is not None
365 return hasattr(cls, "_instance") and cls._instance is not None
366
366
367
367
368 class LoggingConfigurable(Configurable):
368 class LoggingConfigurable(Configurable):
369 """A parent class for Configurables that log.
369 """A parent class for Configurables that log.
370
370
371 Subclasses have a log trait, and the default behavior
371 Subclasses have a log trait, and the default behavior
372 is to get the logger from the currently running Application.
372 is to get the logger from the currently running Application.
373 """
373 """
374
374
375 log = Instance('logging.Logger')
375 log = Instance('logging.Logger')
376 def _log_default(self):
376 def _log_default(self):
377 from IPython.utils import log
377 from IPython.utils import log
378 return log.get_logger()
378 return log.get_logger()
379
379
380
380
@@ -1,837 +1,837 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """A simple configuration system."""
2 """A simple configuration system."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 import argparse
7 import argparse
8 import copy
8 import copy
9 import logging
9 import logging
10 import os
10 import os
11 import re
11 import re
12 import sys
12 import sys
13 import json
13 import json
14 from ast import literal_eval
14 from ast import literal_eval
15
15
16 from IPython.utils.path import filefind, get_ipython_dir
16 from IPython.utils.path import filefind, get_ipython_dir
17 from IPython.utils import py3compat
17 from IPython.utils import py3compat
18 from IPython.utils.encoding import DEFAULT_ENCODING
18 from IPython.utils.encoding import DEFAULT_ENCODING
19 from IPython.utils.py3compat import unicode_type, iteritems
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 # Exceptions
23 # Exceptions
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25
25
26
26
27 class ConfigError(Exception):
27 class ConfigError(Exception):
28 pass
28 pass
29
29
30 class ConfigLoaderError(ConfigError):
30 class ConfigLoaderError(ConfigError):
31 pass
31 pass
32
32
33 class ConfigFileNotFound(ConfigError):
33 class ConfigFileNotFound(ConfigError):
34 pass
34 pass
35
35
36 class ArgumentError(ConfigLoaderError):
36 class ArgumentError(ConfigLoaderError):
37 pass
37 pass
38
38
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40 # Argparse fix
40 # Argparse fix
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42
42
43 # Unfortunately argparse by default prints help messages to stderr instead of
43 # Unfortunately argparse by default prints help messages to stderr instead of
44 # stdout. This makes it annoying to capture long help screens at the command
44 # stdout. This makes it annoying to capture long help screens at the command
45 # line, since one must know how to pipe stderr, which many users don't know how
45 # line, since one must know how to pipe stderr, which many users don't know how
46 # to do. So we override the print_help method with one that defaults to
46 # to do. So we override the print_help method with one that defaults to
47 # stdout and use our class instead.
47 # stdout and use our class instead.
48
48
49 class ArgumentParser(argparse.ArgumentParser):
49 class ArgumentParser(argparse.ArgumentParser):
50 """Simple argparse subclass that prints help to stdout by default."""
50 """Simple argparse subclass that prints help to stdout by default."""
51
51
52 def print_help(self, file=None):
52 def print_help(self, file=None):
53 if file is None:
53 if file is None:
54 file = sys.stdout
54 file = sys.stdout
55 return super(ArgumentParser, self).print_help(file)
55 return super(ArgumentParser, self).print_help(file)
56
56
57 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
57 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
58
58
59 #-----------------------------------------------------------------------------
59 #-----------------------------------------------------------------------------
60 # Config class for holding config information
60 # Config class for holding config information
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62
62
63 class LazyConfigValue(HasTraits):
63 class LazyConfigValue(HasTraits):
64 """Proxy object for exposing methods on configurable containers
64 """Proxy object for exposing methods on configurable containers
65
65
66 Exposes:
66 Exposes:
67
67
68 - append, extend, insert on lists
68 - append, extend, insert on lists
69 - update on dicts
69 - update on dicts
70 - update, add on sets
70 - update, add on sets
71 """
71 """
72
72
73 _value = None
73 _value = None
74
74
75 # list methods
75 # list methods
76 _extend = List()
76 _extend = List()
77 _prepend = List()
77 _prepend = List()
78
78
79 def append(self, obj):
79 def append(self, obj):
80 self._extend.append(obj)
80 self._extend.append(obj)
81
81
82 def extend(self, other):
82 def extend(self, other):
83 self._extend.extend(other)
83 self._extend.extend(other)
84
84
85 def prepend(self, other):
85 def prepend(self, other):
86 """like list.extend, but for the front"""
86 """like list.extend, but for the front"""
87 self._prepend[:0] = other
87 self._prepend[:0] = other
88
88
89 _inserts = List()
89 _inserts = List()
90 def insert(self, index, other):
90 def insert(self, index, other):
91 if not isinstance(index, int):
91 if not isinstance(index, int):
92 raise TypeError("An integer is required")
92 raise TypeError("An integer is required")
93 self._inserts.append((index, other))
93 self._inserts.append((index, other))
94
94
95 # dict methods
95 # dict methods
96 # update is used for both dict and set
96 # update is used for both dict and set
97 _update = Any()
97 _update = Any()
98 def update(self, other):
98 def update(self, other):
99 if self._update is None:
99 if self._update is None:
100 if isinstance(other, dict):
100 if isinstance(other, dict):
101 self._update = {}
101 self._update = {}
102 else:
102 else:
103 self._update = set()
103 self._update = set()
104 self._update.update(other)
104 self._update.update(other)
105
105
106 # set methods
106 # set methods
107 def add(self, obj):
107 def add(self, obj):
108 self.update({obj})
108 self.update({obj})
109
109
110 def get_value(self, initial):
110 def get_value(self, initial):
111 """construct the value from the initial one
111 """construct the value from the initial one
112
112
113 after applying any insert / extend / update changes
113 after applying any insert / extend / update changes
114 """
114 """
115 if self._value is not None:
115 if self._value is not None:
116 return self._value
116 return self._value
117 value = copy.deepcopy(initial)
117 value = copy.deepcopy(initial)
118 if isinstance(value, list):
118 if isinstance(value, list):
119 for idx, obj in self._inserts:
119 for idx, obj in self._inserts:
120 value.insert(idx, obj)
120 value.insert(idx, obj)
121 value[:0] = self._prepend
121 value[:0] = self._prepend
122 value.extend(self._extend)
122 value.extend(self._extend)
123
123
124 elif isinstance(value, dict):
124 elif isinstance(value, dict):
125 if self._update:
125 if self._update:
126 value.update(self._update)
126 value.update(self._update)
127 elif isinstance(value, set):
127 elif isinstance(value, set):
128 if self._update:
128 if self._update:
129 value.update(self._update)
129 value.update(self._update)
130 self._value = value
130 self._value = value
131 return value
131 return value
132
132
133 def to_dict(self):
133 def to_dict(self):
134 """return JSONable dict form of my data
134 """return JSONable dict form of my data
135
135
136 Currently update as dict or set, extend, prepend as lists, and inserts as list of tuples.
136 Currently update as dict or set, extend, prepend as lists, and inserts as list of tuples.
137 """
137 """
138 d = {}
138 d = {}
139 if self._update:
139 if self._update:
140 d['update'] = self._update
140 d['update'] = self._update
141 if self._extend:
141 if self._extend:
142 d['extend'] = self._extend
142 d['extend'] = self._extend
143 if self._prepend:
143 if self._prepend:
144 d['prepend'] = self._prepend
144 d['prepend'] = self._prepend
145 elif self._inserts:
145 elif self._inserts:
146 d['inserts'] = self._inserts
146 d['inserts'] = self._inserts
147 return d
147 return d
148
148
149
149
150 def _is_section_key(key):
150 def _is_section_key(key):
151 """Is a Config key a section name (does it start with a capital)?"""
151 """Is a Config key a section name (does it start with a capital)?"""
152 if key and key[0].upper()==key[0] and not key.startswith('_'):
152 if key and key[0].upper()==key[0] and not key.startswith('_'):
153 return True
153 return True
154 else:
154 else:
155 return False
155 return False
156
156
157
157
158 class Config(dict):
158 class Config(dict):
159 """An attribute based dict that can do smart merges."""
159 """An attribute based dict that can do smart merges."""
160
160
161 def __init__(self, *args, **kwds):
161 def __init__(self, *args, **kwds):
162 dict.__init__(self, *args, **kwds)
162 dict.__init__(self, *args, **kwds)
163 self._ensure_subconfig()
163 self._ensure_subconfig()
164
164
165 def _ensure_subconfig(self):
165 def _ensure_subconfig(self):
166 """ensure that sub-dicts that should be Config objects are
166 """ensure that sub-dicts that should be Config objects are
167
167
168 casts dicts that are under section keys to Config objects,
168 casts dicts that are under section keys to Config objects,
169 which is necessary for constructing Config objects from dict literals.
169 which is necessary for constructing Config objects from dict literals.
170 """
170 """
171 for key in self:
171 for key in self:
172 obj = self[key]
172 obj = self[key]
173 if _is_section_key(key) \
173 if _is_section_key(key) \
174 and isinstance(obj, dict) \
174 and isinstance(obj, dict) \
175 and not isinstance(obj, Config):
175 and not isinstance(obj, Config):
176 setattr(self, key, Config(obj))
176 setattr(self, key, Config(obj))
177
177
178 def _merge(self, other):
178 def _merge(self, other):
179 """deprecated alias, use Config.merge()"""
179 """deprecated alias, use Config.merge()"""
180 self.merge(other)
180 self.merge(other)
181
181
182 def merge(self, other):
182 def merge(self, other):
183 """merge another config object into this one"""
183 """merge another config object into this one"""
184 to_update = {}
184 to_update = {}
185 for k, v in iteritems(other):
185 for k, v in iteritems(other):
186 if k not in self:
186 if k not in self:
187 to_update[k] = copy.deepcopy(v)
187 to_update[k] = copy.deepcopy(v)
188 else: # I have this key
188 else: # I have this key
189 if isinstance(v, Config) and isinstance(self[k], Config):
189 if isinstance(v, Config) and isinstance(self[k], Config):
190 # Recursively merge common sub Configs
190 # Recursively merge common sub Configs
191 self[k].merge(v)
191 self[k].merge(v)
192 else:
192 else:
193 # Plain updates for non-Configs
193 # Plain updates for non-Configs
194 to_update[k] = copy.deepcopy(v)
194 to_update[k] = copy.deepcopy(v)
195
195
196 self.update(to_update)
196 self.update(to_update)
197
197
198 def collisions(self, other):
198 def collisions(self, other):
199 """Check for collisions between two config objects.
199 """Check for collisions between two config objects.
200
200
201 Returns a dict of the form {"Class": {"trait": "collision message"}}`,
201 Returns a dict of the form {"Class": {"trait": "collision message"}}`,
202 indicating which values have been ignored.
202 indicating which values have been ignored.
203
203
204 An empty dict indicates no collisions.
204 An empty dict indicates no collisions.
205 """
205 """
206 collisions = {}
206 collisions = {}
207 for section in self:
207 for section in self:
208 if section not in other:
208 if section not in other:
209 continue
209 continue
210 mine = self[section]
210 mine = self[section]
211 theirs = other[section]
211 theirs = other[section]
212 for key in mine:
212 for key in mine:
213 if key in theirs and mine[key] != theirs[key]:
213 if key in theirs and mine[key] != theirs[key]:
214 collisions.setdefault(section, {})
214 collisions.setdefault(section, {})
215 collisions[section][key] = "%r ignored, using %r" % (mine[key], theirs[key])
215 collisions[section][key] = "%r ignored, using %r" % (mine[key], theirs[key])
216 return collisions
216 return collisions
217
217
218 def __contains__(self, key):
218 def __contains__(self, key):
219 # allow nested contains of the form `"Section.key" in config`
219 # allow nested contains of the form `"Section.key" in config`
220 if '.' in key:
220 if '.' in key:
221 first, remainder = key.split('.', 1)
221 first, remainder = key.split('.', 1)
222 if first not in self:
222 if first not in self:
223 return False
223 return False
224 return remainder in self[first]
224 return remainder in self[first]
225
225
226 return super(Config, self).__contains__(key)
226 return super(Config, self).__contains__(key)
227
227
228 # .has_key is deprecated for dictionaries.
228 # .has_key is deprecated for dictionaries.
229 has_key = __contains__
229 has_key = __contains__
230
230
231 def _has_section(self, key):
231 def _has_section(self, key):
232 return _is_section_key(key) and key in self
232 return _is_section_key(key) and key in self
233
233
234 def copy(self):
234 def copy(self):
235 return type(self)(dict.copy(self))
235 return type(self)(dict.copy(self))
236 # copy nested config objects
236 # copy nested config objects
237 for k, v in self.items():
237 for k, v in self.items():
238 if isinstance(v, Config):
238 if isinstance(v, Config):
239 new_config[k] = v.copy()
239 new_config[k] = v.copy()
240 return new_config
240 return new_config
241
241
242 def __copy__(self):
242 def __copy__(self):
243 return self.copy()
243 return self.copy()
244
244
245 def __deepcopy__(self, memo):
245 def __deepcopy__(self, memo):
246 new_config = type(self)()
246 new_config = type(self)()
247 for key, value in self.items():
247 for key, value in self.items():
248 if isinstance(value, (Config, LazyConfigValue)):
248 if isinstance(value, (Config, LazyConfigValue)):
249 # deep copy config objects
249 # deep copy config objects
250 value = copy.deepcopy(value, memo)
250 value = copy.deepcopy(value, memo)
251 elif type(value) in {dict, list, set, tuple}:
251 elif type(value) in {dict, list, set, tuple}:
252 # shallow copy plain container traits
252 # shallow copy plain container traits
253 value = copy.copy(value)
253 value = copy.copy(value)
254 new_config[key] = value
254 new_config[key] = value
255 return new_config
255 return new_config
256
256
257 def __getitem__(self, key):
257 def __getitem__(self, key):
258 try:
258 try:
259 return dict.__getitem__(self, key)
259 return dict.__getitem__(self, key)
260 except KeyError:
260 except KeyError:
261 if _is_section_key(key):
261 if _is_section_key(key):
262 c = Config()
262 c = Config()
263 dict.__setitem__(self, key, c)
263 dict.__setitem__(self, key, c)
264 return c
264 return c
265 elif not key.startswith('_'):
265 elif not key.startswith('_'):
266 # undefined, create lazy value, used for container methods
266 # undefined, create lazy value, used for container methods
267 v = LazyConfigValue()
267 v = LazyConfigValue()
268 dict.__setitem__(self, key, v)
268 dict.__setitem__(self, key, v)
269 return v
269 return v
270 else:
270 else:
271 raise KeyError
271 raise KeyError
272
272
273 def __setitem__(self, key, value):
273 def __setitem__(self, key, value):
274 if _is_section_key(key):
274 if _is_section_key(key):
275 if not isinstance(value, Config):
275 if not isinstance(value, Config):
276 raise ValueError('values whose keys begin with an uppercase '
276 raise ValueError('values whose keys begin with an uppercase '
277 'char must be Config instances: %r, %r' % (key, value))
277 'char must be Config instances: %r, %r' % (key, value))
278 dict.__setitem__(self, key, value)
278 dict.__setitem__(self, key, value)
279
279
280 def __getattr__(self, key):
280 def __getattr__(self, key):
281 if key.startswith('__'):
281 if key.startswith('__'):
282 return dict.__getattr__(self, key)
282 return dict.__getattr__(self, key)
283 try:
283 try:
284 return self.__getitem__(key)
284 return self.__getitem__(key)
285 except KeyError as e:
285 except KeyError as e:
286 raise AttributeError(e)
286 raise AttributeError(e)
287
287
288 def __setattr__(self, key, value):
288 def __setattr__(self, key, value):
289 if key.startswith('__'):
289 if key.startswith('__'):
290 return dict.__setattr__(self, key, value)
290 return dict.__setattr__(self, key, value)
291 try:
291 try:
292 self.__setitem__(key, value)
292 self.__setitem__(key, value)
293 except KeyError as e:
293 except KeyError as e:
294 raise AttributeError(e)
294 raise AttributeError(e)
295
295
296 def __delattr__(self, key):
296 def __delattr__(self, key):
297 if key.startswith('__'):
297 if key.startswith('__'):
298 return dict.__delattr__(self, key)
298 return dict.__delattr__(self, key)
299 try:
299 try:
300 dict.__delitem__(self, key)
300 dict.__delitem__(self, key)
301 except KeyError as e:
301 except KeyError as e:
302 raise AttributeError(e)
302 raise AttributeError(e)
303
303
304
304
305 #-----------------------------------------------------------------------------
305 #-----------------------------------------------------------------------------
306 # Config loading classes
306 # Config loading classes
307 #-----------------------------------------------------------------------------
307 #-----------------------------------------------------------------------------
308
308
309
309
310 class ConfigLoader(object):
310 class ConfigLoader(object):
311 """A object for loading configurations from just about anywhere.
311 """A object for loading configurations from just about anywhere.
312
312
313 The resulting configuration is packaged as a :class:`Config`.
313 The resulting configuration is packaged as a :class:`Config`.
314
314
315 Notes
315 Notes
316 -----
316 -----
317 A :class:`ConfigLoader` does one thing: load a config from a source
317 A :class:`ConfigLoader` does one thing: load a config from a source
318 (file, command line arguments) and returns the data as a :class:`Config` object.
318 (file, command line arguments) and returns the data as a :class:`Config` object.
319 There are lots of things that :class:`ConfigLoader` does not do. It does
319 There are lots of things that :class:`ConfigLoader` does not do. It does
320 not implement complex logic for finding config files. It does not handle
320 not implement complex logic for finding config files. It does not handle
321 default values or merge multiple configs. These things need to be
321 default values or merge multiple configs. These things need to be
322 handled elsewhere.
322 handled elsewhere.
323 """
323 """
324
324
325 def _log_default(self):
325 def _log_default(self):
326 from IPython.utils.log import get_logger
326 from IPython.utils.log import get_logger
327 return get_logger()
327 return get_logger()
328
328
329 def __init__(self, log=None):
329 def __init__(self, log=None):
330 """A base class for config loaders.
330 """A base class for config loaders.
331
331
332 log : instance of :class:`logging.Logger` to use.
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 will be used
334 will be used
335
335
336 Examples
336 Examples
337 --------
337 --------
338
338
339 >>> cl = ConfigLoader()
339 >>> cl = ConfigLoader()
340 >>> config = cl.load_config()
340 >>> config = cl.load_config()
341 >>> config
341 >>> config
342 {}
342 {}
343 """
343 """
344 self.clear()
344 self.clear()
345 if log is None:
345 if log is None:
346 self.log = self._log_default()
346 self.log = self._log_default()
347 self.log.debug('Using default logger')
347 self.log.debug('Using default logger')
348 else:
348 else:
349 self.log = log
349 self.log = log
350
350
351 def clear(self):
351 def clear(self):
352 self.config = Config()
352 self.config = Config()
353
353
354 def load_config(self):
354 def load_config(self):
355 """Load a config from somewhere, return a :class:`Config` instance.
355 """Load a config from somewhere, return a :class:`Config` instance.
356
356
357 Usually, this will cause self.config to be set and then returned.
357 Usually, this will cause self.config to be set and then returned.
358 However, in most cases, :meth:`ConfigLoader.clear` should be called
358 However, in most cases, :meth:`ConfigLoader.clear` should be called
359 to erase any previous state.
359 to erase any previous state.
360 """
360 """
361 self.clear()
361 self.clear()
362 return self.config
362 return self.config
363
363
364
364
365 class FileConfigLoader(ConfigLoader):
365 class FileConfigLoader(ConfigLoader):
366 """A base class for file based configurations.
366 """A base class for file based configurations.
367
367
368 As we add more file based config loaders, the common logic should go
368 As we add more file based config loaders, the common logic should go
369 here.
369 here.
370 """
370 """
371
371
372 def __init__(self, filename, path=None, **kw):
372 def __init__(self, filename, path=None, **kw):
373 """Build a config loader for a filename and path.
373 """Build a config loader for a filename and path.
374
374
375 Parameters
375 Parameters
376 ----------
376 ----------
377 filename : str
377 filename : str
378 The file name of the config file.
378 The file name of the config file.
379 path : str, list, tuple
379 path : str, list, tuple
380 The path to search for the config file on, or a sequence of
380 The path to search for the config file on, or a sequence of
381 paths to try in order.
381 paths to try in order.
382 """
382 """
383 super(FileConfigLoader, self).__init__(**kw)
383 super(FileConfigLoader, self).__init__(**kw)
384 self.filename = filename
384 self.filename = filename
385 self.path = path
385 self.path = path
386 self.full_filename = ''
386 self.full_filename = ''
387
387
388 def _find_file(self):
388 def _find_file(self):
389 """Try to find the file by searching the paths."""
389 """Try to find the file by searching the paths."""
390 self.full_filename = filefind(self.filename, self.path)
390 self.full_filename = filefind(self.filename, self.path)
391
391
392 class JSONFileConfigLoader(FileConfigLoader):
392 class JSONFileConfigLoader(FileConfigLoader):
393 """A JSON file loader for config"""
393 """A JSON file loader for config"""
394
394
395 def load_config(self):
395 def load_config(self):
396 """Load the config from a file and return it as a Config object."""
396 """Load the config from a file and return it as a Config object."""
397 self.clear()
397 self.clear()
398 try:
398 try:
399 self._find_file()
399 self._find_file()
400 except IOError as e:
400 except IOError as e:
401 raise ConfigFileNotFound(str(e))
401 raise ConfigFileNotFound(str(e))
402 dct = self._read_file_as_dict()
402 dct = self._read_file_as_dict()
403 self.config = self._convert_to_config(dct)
403 self.config = self._convert_to_config(dct)
404 return self.config
404 return self.config
405
405
406 def _read_file_as_dict(self):
406 def _read_file_as_dict(self):
407 with open(self.full_filename) as f:
407 with open(self.full_filename) as f:
408 return json.load(f)
408 return json.load(f)
409
409
410 def _convert_to_config(self, dictionary):
410 def _convert_to_config(self, dictionary):
411 if 'version' in dictionary:
411 if 'version' in dictionary:
412 version = dictionary.pop('version')
412 version = dictionary.pop('version')
413 else:
413 else:
414 version = 1
414 version = 1
415 self.log.warn("Unrecognized JSON config file version, assuming version {}".format(version))
415 self.log.warn("Unrecognized JSON config file version, assuming version {}".format(version))
416
416
417 if version == 1:
417 if version == 1:
418 return Config(dictionary)
418 return Config(dictionary)
419 else:
419 else:
420 raise ValueError('Unknown version of JSON config file: {version}'.format(version=version))
420 raise ValueError('Unknown version of JSON config file: {version}'.format(version=version))
421
421
422
422
423 class PyFileConfigLoader(FileConfigLoader):
423 class PyFileConfigLoader(FileConfigLoader):
424 """A config loader for pure python files.
424 """A config loader for pure python files.
425
425
426 This is responsible for locating a Python config file by filename and
426 This is responsible for locating a Python config file by filename and
427 path, then executing it to construct a Config object.
427 path, then executing it to construct a Config object.
428 """
428 """
429
429
430 def load_config(self):
430 def load_config(self):
431 """Load the config from a file and return it as a Config object."""
431 """Load the config from a file and return it as a Config object."""
432 self.clear()
432 self.clear()
433 try:
433 try:
434 self._find_file()
434 self._find_file()
435 except IOError as e:
435 except IOError as e:
436 raise ConfigFileNotFound(str(e))
436 raise ConfigFileNotFound(str(e))
437 self._read_file_as_dict()
437 self._read_file_as_dict()
438 return self.config
438 return self.config
439
439
440 def load_subconfig(self, fname, path=None):
440 def load_subconfig(self, fname, path=None):
441 """Injected into config file namespace as load_subconfig"""
441 """Injected into config file namespace as load_subconfig"""
442 if path is None:
442 if path is None:
443 path = self.path
443 path = self.path
444
444
445 loader = self.__class__(fname, path)
445 loader = self.__class__(fname, path)
446 try:
446 try:
447 sub_config = loader.load_config()
447 sub_config = loader.load_config()
448 except ConfigFileNotFound:
448 except ConfigFileNotFound:
449 # Pass silently if the sub config is not there,
449 # Pass silently if the sub config is not there,
450 # treat it as an empty config file.
450 # treat it as an empty config file.
451 pass
451 pass
452 else:
452 else:
453 self.config.merge(sub_config)
453 self.config.merge(sub_config)
454
454
455 def _read_file_as_dict(self):
455 def _read_file_as_dict(self):
456 """Load the config file into self.config, with recursive loading."""
456 """Load the config file into self.config, with recursive loading."""
457 def get_config():
457 def get_config():
458 """Unnecessary now, but a deprecation warning is more trouble than it's worth."""
458 """Unnecessary now, but a deprecation warning is more trouble than it's worth."""
459 return self.config
459 return self.config
460
460
461 namespace = dict(
461 namespace = dict(
462 c=self.config,
462 c=self.config,
463 load_subconfig=self.load_subconfig,
463 load_subconfig=self.load_subconfig,
464 get_config=get_config,
464 get_config=get_config,
465 __file__=self.full_filename,
465 __file__=self.full_filename,
466 )
466 )
467 fs_encoding = sys.getfilesystemencoding() or 'ascii'
467 fs_encoding = sys.getfilesystemencoding() or 'ascii'
468 conf_filename = self.full_filename.encode(fs_encoding)
468 conf_filename = self.full_filename.encode(fs_encoding)
469 py3compat.execfile(conf_filename, namespace)
469 py3compat.execfile(conf_filename, namespace)
470
470
471
471
472 class CommandLineConfigLoader(ConfigLoader):
472 class CommandLineConfigLoader(ConfigLoader):
473 """A config loader for command line arguments.
473 """A config loader for command line arguments.
474
474
475 As we add more command line based loaders, the common logic should go
475 As we add more command line based loaders, the common logic should go
476 here.
476 here.
477 """
477 """
478
478
479 def _exec_config_str(self, lhs, rhs):
479 def _exec_config_str(self, lhs, rhs):
480 """execute self.config.<lhs> = <rhs>
480 """execute self.config.<lhs> = <rhs>
481
481
482 * expands ~ with expanduser
482 * expands ~ with expanduser
483 * tries to assign with literal_eval, otherwise assigns with just the string,
483 * tries to assign with literal_eval, otherwise assigns with just the string,
484 allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not*
484 allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not*
485 equivalent are `--C.a=4` and `--C.a='4'`.
485 equivalent are `--C.a=4` and `--C.a='4'`.
486 """
486 """
487 rhs = os.path.expanduser(rhs)
487 rhs = os.path.expanduser(rhs)
488 try:
488 try:
489 # Try to see if regular Python syntax will work. This
489 # Try to see if regular Python syntax will work. This
490 # won't handle strings as the quote marks are removed
490 # won't handle strings as the quote marks are removed
491 # by the system shell.
491 # by the system shell.
492 value = literal_eval(rhs)
492 value = literal_eval(rhs)
493 except (NameError, SyntaxError, ValueError):
493 except (NameError, SyntaxError, ValueError):
494 # This case happens if the rhs is a string.
494 # This case happens if the rhs is a string.
495 value = rhs
495 value = rhs
496
496
497 exec(u'self.config.%s = value' % lhs)
497 exec(u'self.config.%s = value' % lhs)
498
498
499 def _load_flag(self, cfg):
499 def _load_flag(self, cfg):
500 """update self.config from a flag, which can be a dict or Config"""
500 """update self.config from a flag, which can be a dict or Config"""
501 if isinstance(cfg, (dict, Config)):
501 if isinstance(cfg, (dict, Config)):
502 # don't clobber whole config sections, update
502 # don't clobber whole config sections, update
503 # each section from config:
503 # each section from config:
504 for sec,c in iteritems(cfg):
504 for sec,c in iteritems(cfg):
505 self.config[sec].update(c)
505 self.config[sec].update(c)
506 else:
506 else:
507 raise TypeError("Invalid flag: %r" % cfg)
507 raise TypeError("Invalid flag: %r" % cfg)
508
508
509 # raw --identifier=value pattern
509 # raw --identifier=value pattern
510 # but *also* accept '-' as wordsep, for aliases
510 # but *also* accept '-' as wordsep, for aliases
511 # accepts: --foo=a
511 # accepts: --foo=a
512 # --Class.trait=value
512 # --Class.trait=value
513 # --alias-name=value
513 # --alias-name=value
514 # rejects: -foo=value
514 # rejects: -foo=value
515 # --foo
515 # --foo
516 # --Class.trait
516 # --Class.trait
517 kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*')
517 kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*')
518
518
519 # just flags, no assignments, with two *or one* leading '-'
519 # just flags, no assignments, with two *or one* leading '-'
520 # accepts: --foo
520 # accepts: --foo
521 # -foo-bar-again
521 # -foo-bar-again
522 # rejects: --anything=anything
522 # rejects: --anything=anything
523 # --two.word
523 # --two.word
524
524
525 flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$')
525 flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$')
526
526
527 class KeyValueConfigLoader(CommandLineConfigLoader):
527 class KeyValueConfigLoader(CommandLineConfigLoader):
528 """A config loader that loads key value pairs from the command line.
528 """A config loader that loads key value pairs from the command line.
529
529
530 This allows command line options to be gives in the following form::
530 This allows command line options to be gives in the following form::
531
531
532 ipython --profile="foo" --InteractiveShell.autocall=False
532 ipython --profile="foo" --InteractiveShell.autocall=False
533 """
533 """
534
534
535 def __init__(self, argv=None, aliases=None, flags=None, **kw):
535 def __init__(self, argv=None, aliases=None, flags=None, **kw):
536 """Create a key value pair config loader.
536 """Create a key value pair config loader.
537
537
538 Parameters
538 Parameters
539 ----------
539 ----------
540 argv : list
540 argv : list
541 A list that has the form of sys.argv[1:] which has unicode
541 A list that has the form of sys.argv[1:] which has unicode
542 elements of the form u"key=value". If this is None (default),
542 elements of the form u"key=value". If this is None (default),
543 then sys.argv[1:] will be used.
543 then sys.argv[1:] will be used.
544 aliases : dict
544 aliases : dict
545 A dict of aliases for configurable traits.
545 A dict of aliases for configurable traits.
546 Keys are the short aliases, Values are the resolved trait.
546 Keys are the short aliases, Values are the resolved trait.
547 Of the form: `{'alias' : 'Configurable.trait'}`
547 Of the form: `{'alias' : 'Configurable.trait'}`
548 flags : dict
548 flags : dict
549 A dict of flags, keyed by str name. Vaues can be Config objects,
549 A dict of flags, keyed by str name. Vaues can be Config objects,
550 dicts, or "key=value" strings. If Config or dict, when the flag
550 dicts, or "key=value" strings. If Config or dict, when the flag
551 is triggered, The flag is loaded as `self.config.update(m)`.
551 is triggered, The flag is loaded as `self.config.update(m)`.
552
552
553 Returns
553 Returns
554 -------
554 -------
555 config : Config
555 config : Config
556 The resulting Config object.
556 The resulting Config object.
557
557
558 Examples
558 Examples
559 --------
559 --------
560
560
561 >>> from IPython.config.loader import KeyValueConfigLoader
561 >>> from traitlets.config.loader import KeyValueConfigLoader
562 >>> cl = KeyValueConfigLoader()
562 >>> cl = KeyValueConfigLoader()
563 >>> d = cl.load_config(["--A.name='brian'","--B.number=0"])
563 >>> d = cl.load_config(["--A.name='brian'","--B.number=0"])
564 >>> sorted(d.items())
564 >>> sorted(d.items())
565 [('A', {'name': 'brian'}), ('B', {'number': 0})]
565 [('A', {'name': 'brian'}), ('B', {'number': 0})]
566 """
566 """
567 super(KeyValueConfigLoader, self).__init__(**kw)
567 super(KeyValueConfigLoader, self).__init__(**kw)
568 if argv is None:
568 if argv is None:
569 argv = sys.argv[1:]
569 argv = sys.argv[1:]
570 self.argv = argv
570 self.argv = argv
571 self.aliases = aliases or {}
571 self.aliases = aliases or {}
572 self.flags = flags or {}
572 self.flags = flags or {}
573
573
574
574
575 def clear(self):
575 def clear(self):
576 super(KeyValueConfigLoader, self).clear()
576 super(KeyValueConfigLoader, self).clear()
577 self.extra_args = []
577 self.extra_args = []
578
578
579
579
580 def _decode_argv(self, argv, enc=None):
580 def _decode_argv(self, argv, enc=None):
581 """decode argv if bytes, using stdin.encoding, falling back on default enc"""
581 """decode argv if bytes, using stdin.encoding, falling back on default enc"""
582 uargv = []
582 uargv = []
583 if enc is None:
583 if enc is None:
584 enc = DEFAULT_ENCODING
584 enc = DEFAULT_ENCODING
585 for arg in argv:
585 for arg in argv:
586 if not isinstance(arg, unicode_type):
586 if not isinstance(arg, unicode_type):
587 # only decode if not already decoded
587 # only decode if not already decoded
588 arg = arg.decode(enc)
588 arg = arg.decode(enc)
589 uargv.append(arg)
589 uargv.append(arg)
590 return uargv
590 return uargv
591
591
592
592
593 def load_config(self, argv=None, aliases=None, flags=None):
593 def load_config(self, argv=None, aliases=None, flags=None):
594 """Parse the configuration and generate the Config object.
594 """Parse the configuration and generate the Config object.
595
595
596 After loading, any arguments that are not key-value or
596 After loading, any arguments that are not key-value or
597 flags will be stored in self.extra_args - a list of
597 flags will be stored in self.extra_args - a list of
598 unparsed command-line arguments. This is used for
598 unparsed command-line arguments. This is used for
599 arguments such as input files or subcommands.
599 arguments such as input files or subcommands.
600
600
601 Parameters
601 Parameters
602 ----------
602 ----------
603 argv : list, optional
603 argv : list, optional
604 A list that has the form of sys.argv[1:] which has unicode
604 A list that has the form of sys.argv[1:] which has unicode
605 elements of the form u"key=value". If this is None (default),
605 elements of the form u"key=value". If this is None (default),
606 then self.argv will be used.
606 then self.argv will be used.
607 aliases : dict
607 aliases : dict
608 A dict of aliases for configurable traits.
608 A dict of aliases for configurable traits.
609 Keys are the short aliases, Values are the resolved trait.
609 Keys are the short aliases, Values are the resolved trait.
610 Of the form: `{'alias' : 'Configurable.trait'}`
610 Of the form: `{'alias' : 'Configurable.trait'}`
611 flags : dict
611 flags : dict
612 A dict of flags, keyed by str name. Values can be Config objects
612 A dict of flags, keyed by str name. Values can be Config objects
613 or dicts. When the flag is triggered, The config is loaded as
613 or dicts. When the flag is triggered, The config is loaded as
614 `self.config.update(cfg)`.
614 `self.config.update(cfg)`.
615 """
615 """
616 self.clear()
616 self.clear()
617 if argv is None:
617 if argv is None:
618 argv = self.argv
618 argv = self.argv
619 if aliases is None:
619 if aliases is None:
620 aliases = self.aliases
620 aliases = self.aliases
621 if flags is None:
621 if flags is None:
622 flags = self.flags
622 flags = self.flags
623
623
624 # ensure argv is a list of unicode strings:
624 # ensure argv is a list of unicode strings:
625 uargv = self._decode_argv(argv)
625 uargv = self._decode_argv(argv)
626 for idx,raw in enumerate(uargv):
626 for idx,raw in enumerate(uargv):
627 # strip leading '-'
627 # strip leading '-'
628 item = raw.lstrip('-')
628 item = raw.lstrip('-')
629
629
630 if raw == '--':
630 if raw == '--':
631 # don't parse arguments after '--'
631 # don't parse arguments after '--'
632 # this is useful for relaying arguments to scripts, e.g.
632 # this is useful for relaying arguments to scripts, e.g.
633 # ipython -i foo.py --matplotlib=qt -- args after '--' go-to-foo.py
633 # ipython -i foo.py --matplotlib=qt -- args after '--' go-to-foo.py
634 self.extra_args.extend(uargv[idx+1:])
634 self.extra_args.extend(uargv[idx+1:])
635 break
635 break
636
636
637 if kv_pattern.match(raw):
637 if kv_pattern.match(raw):
638 lhs,rhs = item.split('=',1)
638 lhs,rhs = item.split('=',1)
639 # Substitute longnames for aliases.
639 # Substitute longnames for aliases.
640 if lhs in aliases:
640 if lhs in aliases:
641 lhs = aliases[lhs]
641 lhs = aliases[lhs]
642 if '.' not in lhs:
642 if '.' not in lhs:
643 # probably a mistyped alias, but not technically illegal
643 # probably a mistyped alias, but not technically illegal
644 self.log.warn("Unrecognized alias: '%s', it will probably have no effect.", raw)
644 self.log.warn("Unrecognized alias: '%s', it will probably have no effect.", raw)
645 try:
645 try:
646 self._exec_config_str(lhs, rhs)
646 self._exec_config_str(lhs, rhs)
647 except Exception:
647 except Exception:
648 raise ArgumentError("Invalid argument: '%s'" % raw)
648 raise ArgumentError("Invalid argument: '%s'" % raw)
649
649
650 elif flag_pattern.match(raw):
650 elif flag_pattern.match(raw):
651 if item in flags:
651 if item in flags:
652 cfg,help = flags[item]
652 cfg,help = flags[item]
653 self._load_flag(cfg)
653 self._load_flag(cfg)
654 else:
654 else:
655 raise ArgumentError("Unrecognized flag: '%s'"%raw)
655 raise ArgumentError("Unrecognized flag: '%s'"%raw)
656 elif raw.startswith('-'):
656 elif raw.startswith('-'):
657 kv = '--'+item
657 kv = '--'+item
658 if kv_pattern.match(kv):
658 if kv_pattern.match(kv):
659 raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv))
659 raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv))
660 else:
660 else:
661 raise ArgumentError("Invalid argument: '%s'"%raw)
661 raise ArgumentError("Invalid argument: '%s'"%raw)
662 else:
662 else:
663 # keep all args that aren't valid in a list,
663 # keep all args that aren't valid in a list,
664 # in case our parent knows what to do with them.
664 # in case our parent knows what to do with them.
665 self.extra_args.append(item)
665 self.extra_args.append(item)
666 return self.config
666 return self.config
667
667
668 class ArgParseConfigLoader(CommandLineConfigLoader):
668 class ArgParseConfigLoader(CommandLineConfigLoader):
669 """A loader that uses the argparse module to load from the command line."""
669 """A loader that uses the argparse module to load from the command line."""
670
670
671 def __init__(self, argv=None, aliases=None, flags=None, log=None, *parser_args, **parser_kw):
671 def __init__(self, argv=None, aliases=None, flags=None, log=None, *parser_args, **parser_kw):
672 """Create a config loader for use with argparse.
672 """Create a config loader for use with argparse.
673
673
674 Parameters
674 Parameters
675 ----------
675 ----------
676
676
677 argv : optional, list
677 argv : optional, list
678 If given, used to read command-line arguments from, otherwise
678 If given, used to read command-line arguments from, otherwise
679 sys.argv[1:] is used.
679 sys.argv[1:] is used.
680
680
681 parser_args : tuple
681 parser_args : tuple
682 A tuple of positional arguments that will be passed to the
682 A tuple of positional arguments that will be passed to the
683 constructor of :class:`argparse.ArgumentParser`.
683 constructor of :class:`argparse.ArgumentParser`.
684
684
685 parser_kw : dict
685 parser_kw : dict
686 A tuple of keyword arguments that will be passed to the
686 A tuple of keyword arguments that will be passed to the
687 constructor of :class:`argparse.ArgumentParser`.
687 constructor of :class:`argparse.ArgumentParser`.
688
688
689 Returns
689 Returns
690 -------
690 -------
691 config : Config
691 config : Config
692 The resulting Config object.
692 The resulting Config object.
693 """
693 """
694 super(CommandLineConfigLoader, self).__init__(log=log)
694 super(CommandLineConfigLoader, self).__init__(log=log)
695 self.clear()
695 self.clear()
696 if argv is None:
696 if argv is None:
697 argv = sys.argv[1:]
697 argv = sys.argv[1:]
698 self.argv = argv
698 self.argv = argv
699 self.aliases = aliases or {}
699 self.aliases = aliases or {}
700 self.flags = flags or {}
700 self.flags = flags or {}
701
701
702 self.parser_args = parser_args
702 self.parser_args = parser_args
703 self.version = parser_kw.pop("version", None)
703 self.version = parser_kw.pop("version", None)
704 kwargs = dict(argument_default=argparse.SUPPRESS)
704 kwargs = dict(argument_default=argparse.SUPPRESS)
705 kwargs.update(parser_kw)
705 kwargs.update(parser_kw)
706 self.parser_kw = kwargs
706 self.parser_kw = kwargs
707
707
708 def load_config(self, argv=None, aliases=None, flags=None):
708 def load_config(self, argv=None, aliases=None, flags=None):
709 """Parse command line arguments and return as a Config object.
709 """Parse command line arguments and return as a Config object.
710
710
711 Parameters
711 Parameters
712 ----------
712 ----------
713
713
714 args : optional, list
714 args : optional, list
715 If given, a list with the structure of sys.argv[1:] to parse
715 If given, a list with the structure of sys.argv[1:] to parse
716 arguments from. If not given, the instance's self.argv attribute
716 arguments from. If not given, the instance's self.argv attribute
717 (given at construction time) is used."""
717 (given at construction time) is used."""
718 self.clear()
718 self.clear()
719 if argv is None:
719 if argv is None:
720 argv = self.argv
720 argv = self.argv
721 if aliases is None:
721 if aliases is None:
722 aliases = self.aliases
722 aliases = self.aliases
723 if flags is None:
723 if flags is None:
724 flags = self.flags
724 flags = self.flags
725 self._create_parser(aliases, flags)
725 self._create_parser(aliases, flags)
726 self._parse_args(argv)
726 self._parse_args(argv)
727 self._convert_to_config()
727 self._convert_to_config()
728 return self.config
728 return self.config
729
729
730 def get_extra_args(self):
730 def get_extra_args(self):
731 if hasattr(self, 'extra_args'):
731 if hasattr(self, 'extra_args'):
732 return self.extra_args
732 return self.extra_args
733 else:
733 else:
734 return []
734 return []
735
735
736 def _create_parser(self, aliases=None, flags=None):
736 def _create_parser(self, aliases=None, flags=None):
737 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
737 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
738 self._add_arguments(aliases, flags)
738 self._add_arguments(aliases, flags)
739
739
740 def _add_arguments(self, aliases=None, flags=None):
740 def _add_arguments(self, aliases=None, flags=None):
741 raise NotImplementedError("subclasses must implement _add_arguments")
741 raise NotImplementedError("subclasses must implement _add_arguments")
742
742
743 def _parse_args(self, args):
743 def _parse_args(self, args):
744 """self.parser->self.parsed_data"""
744 """self.parser->self.parsed_data"""
745 # decode sys.argv to support unicode command-line options
745 # decode sys.argv to support unicode command-line options
746 enc = DEFAULT_ENCODING
746 enc = DEFAULT_ENCODING
747 uargs = [py3compat.cast_unicode(a, enc) for a in args]
747 uargs = [py3compat.cast_unicode(a, enc) for a in args]
748 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
748 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
749
749
750 def _convert_to_config(self):
750 def _convert_to_config(self):
751 """self.parsed_data->self.config"""
751 """self.parsed_data->self.config"""
752 for k, v in iteritems(vars(self.parsed_data)):
752 for k, v in iteritems(vars(self.parsed_data)):
753 exec("self.config.%s = v"%k, locals(), globals())
753 exec("self.config.%s = v"%k, locals(), globals())
754
754
755 class KVArgParseConfigLoader(ArgParseConfigLoader):
755 class KVArgParseConfigLoader(ArgParseConfigLoader):
756 """A config loader that loads aliases and flags with argparse,
756 """A config loader that loads aliases and flags with argparse,
757 but will use KVLoader for the rest. This allows better parsing
757 but will use KVLoader for the rest. This allows better parsing
758 of common args, such as `ipython -c 'print 5'`, but still gets
758 of common args, such as `ipython -c 'print 5'`, but still gets
759 arbitrary config with `ipython --InteractiveShell.use_readline=False`"""
759 arbitrary config with `ipython --InteractiveShell.use_readline=False`"""
760
760
761 def _add_arguments(self, aliases=None, flags=None):
761 def _add_arguments(self, aliases=None, flags=None):
762 self.alias_flags = {}
762 self.alias_flags = {}
763 # print aliases, flags
763 # print aliases, flags
764 if aliases is None:
764 if aliases is None:
765 aliases = self.aliases
765 aliases = self.aliases
766 if flags is None:
766 if flags is None:
767 flags = self.flags
767 flags = self.flags
768 paa = self.parser.add_argument
768 paa = self.parser.add_argument
769 for key,value in iteritems(aliases):
769 for key,value in iteritems(aliases):
770 if key in flags:
770 if key in flags:
771 # flags
771 # flags
772 nargs = '?'
772 nargs = '?'
773 else:
773 else:
774 nargs = None
774 nargs = None
775 if len(key) is 1:
775 if len(key) is 1:
776 paa('-'+key, '--'+key, type=unicode_type, dest=value, nargs=nargs)
776 paa('-'+key, '--'+key, type=unicode_type, dest=value, nargs=nargs)
777 else:
777 else:
778 paa('--'+key, type=unicode_type, dest=value, nargs=nargs)
778 paa('--'+key, type=unicode_type, dest=value, nargs=nargs)
779 for key, (value, help) in iteritems(flags):
779 for key, (value, help) in iteritems(flags):
780 if key in self.aliases:
780 if key in self.aliases:
781 #
781 #
782 self.alias_flags[self.aliases[key]] = value
782 self.alias_flags[self.aliases[key]] = value
783 continue
783 continue
784 if len(key) is 1:
784 if len(key) is 1:
785 paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
785 paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
786 else:
786 else:
787 paa('--'+key, action='append_const', dest='_flags', const=value)
787 paa('--'+key, action='append_const', dest='_flags', const=value)
788
788
789 def _convert_to_config(self):
789 def _convert_to_config(self):
790 """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""
790 """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""
791 # remove subconfigs list from namespace before transforming the Namespace
791 # remove subconfigs list from namespace before transforming the Namespace
792 if '_flags' in self.parsed_data:
792 if '_flags' in self.parsed_data:
793 subcs = self.parsed_data._flags
793 subcs = self.parsed_data._flags
794 del self.parsed_data._flags
794 del self.parsed_data._flags
795 else:
795 else:
796 subcs = []
796 subcs = []
797
797
798 for k, v in iteritems(vars(self.parsed_data)):
798 for k, v in iteritems(vars(self.parsed_data)):
799 if v is None:
799 if v is None:
800 # it was a flag that shares the name of an alias
800 # it was a flag that shares the name of an alias
801 subcs.append(self.alias_flags[k])
801 subcs.append(self.alias_flags[k])
802 else:
802 else:
803 # eval the KV assignment
803 # eval the KV assignment
804 self._exec_config_str(k, v)
804 self._exec_config_str(k, v)
805
805
806 for subc in subcs:
806 for subc in subcs:
807 self._load_flag(subc)
807 self._load_flag(subc)
808
808
809 if self.extra_args:
809 if self.extra_args:
810 sub_parser = KeyValueConfigLoader(log=self.log)
810 sub_parser = KeyValueConfigLoader(log=self.log)
811 sub_parser.load_config(self.extra_args)
811 sub_parser.load_config(self.extra_args)
812 self.config.merge(sub_parser.config)
812 self.config.merge(sub_parser.config)
813 self.extra_args = sub_parser.extra_args
813 self.extra_args = sub_parser.extra_args
814
814
815
815
816 def load_pyconfig_files(config_files, path):
816 def load_pyconfig_files(config_files, path):
817 """Load multiple Python config files, merging each of them in turn.
817 """Load multiple Python config files, merging each of them in turn.
818
818
819 Parameters
819 Parameters
820 ==========
820 ==========
821 config_files : list of str
821 config_files : list of str
822 List of config files names to load and merge into the config.
822 List of config files names to load and merge into the config.
823 path : unicode
823 path : unicode
824 The full path to the location of the config files.
824 The full path to the location of the config files.
825 """
825 """
826 config = Config()
826 config = Config()
827 for cf in config_files:
827 for cf in config_files:
828 loader = PyFileConfigLoader(cf, path=path)
828 loader = PyFileConfigLoader(cf, path=path)
829 try:
829 try:
830 next_config = loader.load_config()
830 next_config = loader.load_config()
831 except ConfigFileNotFound:
831 except ConfigFileNotFound:
832 pass
832 pass
833 except:
833 except:
834 raise
834 raise
835 else:
835 else:
836 config.merge(next_config)
836 config.merge(next_config)
837 return config
837 return config
@@ -1,88 +1,88 b''
1 """Manager to read and modify config data in JSON files.
1 """Manager to read and modify config data in JSON files.
2 """
2 """
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5 import errno
5 import errno
6 import io
6 import io
7 import json
7 import json
8 import os
8 import os
9
9
10 from IPython.config import LoggingConfigurable
10 from traitlets.config import LoggingConfigurable
11 from IPython.utils.py3compat import PY3
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 def recursive_update(target, new):
15 def recursive_update(target, new):
16 """Recursively update one dictionary using another.
16 """Recursively update one dictionary using another.
17
17
18 None values will delete their keys.
18 None values will delete their keys.
19 """
19 """
20 for k, v in new.items():
20 for k, v in new.items():
21 if isinstance(v, dict):
21 if isinstance(v, dict):
22 if k not in target:
22 if k not in target:
23 target[k] = {}
23 target[k] = {}
24 recursive_update(target[k], v)
24 recursive_update(target[k], v)
25 if not target[k]:
25 if not target[k]:
26 # Prune empty subdicts
26 # Prune empty subdicts
27 del target[k]
27 del target[k]
28
28
29 elif v is None:
29 elif v is None:
30 target.pop(k, None)
30 target.pop(k, None)
31
31
32 else:
32 else:
33 target[k] = v
33 target[k] = v
34
34
35
35
36 class BaseJSONConfigManager(LoggingConfigurable):
36 class BaseJSONConfigManager(LoggingConfigurable):
37 """General JSON config manager
37 """General JSON config manager
38
38
39 Deals with persisting/storing config in a json file
39 Deals with persisting/storing config in a json file
40 """
40 """
41
41
42 config_dir = Unicode('.')
42 config_dir = Unicode('.')
43
43
44 def ensure_config_dir_exists(self):
44 def ensure_config_dir_exists(self):
45 try:
45 try:
46 os.mkdir(self.config_dir, 0o755)
46 os.mkdir(self.config_dir, 0o755)
47 except OSError as e:
47 except OSError as e:
48 if e.errno != errno.EEXIST:
48 if e.errno != errno.EEXIST:
49 raise
49 raise
50
50
51 def file_name(self, section_name):
51 def file_name(self, section_name):
52 return os.path.join(self.config_dir, section_name+'.json')
52 return os.path.join(self.config_dir, section_name+'.json')
53
53
54 def get(self, section_name):
54 def get(self, section_name):
55 """Retrieve the config data for the specified section.
55 """Retrieve the config data for the specified section.
56
56
57 Returns the data as a dictionary, or an empty dictionary if the file
57 Returns the data as a dictionary, or an empty dictionary if the file
58 doesn't exist.
58 doesn't exist.
59 """
59 """
60 filename = self.file_name(section_name)
60 filename = self.file_name(section_name)
61 if os.path.isfile(filename):
61 if os.path.isfile(filename):
62 with io.open(filename, encoding='utf-8') as f:
62 with io.open(filename, encoding='utf-8') as f:
63 return json.load(f)
63 return json.load(f)
64 else:
64 else:
65 return {}
65 return {}
66
66
67 def set(self, section_name, data):
67 def set(self, section_name, data):
68 """Store the given config data.
68 """Store the given config data.
69 """
69 """
70 filename = self.file_name(section_name)
70 filename = self.file_name(section_name)
71 self.ensure_config_dir_exists()
71 self.ensure_config_dir_exists()
72
72
73 if PY3:
73 if PY3:
74 f = io.open(filename, 'w', encoding='utf-8')
74 f = io.open(filename, 'w', encoding='utf-8')
75 else:
75 else:
76 f = open(filename, 'wb')
76 f = open(filename, 'wb')
77 with f:
77 with f:
78 json.dump(data, f, indent=2)
78 json.dump(data, f, indent=2)
79
79
80 def update(self, section_name, new_data):
80 def update(self, section_name, new_data):
81 """Modify the config section by recursively updating it with new_data.
81 """Modify the config section by recursively updating it with new_data.
82
82
83 Returns the modified config data as a dictionary.
83 Returns the modified config data as a dictionary.
84 """
84 """
85 data = self.get(section_name)
85 data = self.get(section_name)
86 recursive_update(data, new_data)
86 recursive_update(data, new_data)
87 self.set(section_name, data)
87 self.set(section_name, data)
88 return data
88 return data
1 NO CONTENT: file renamed from IPython/config/tests/__init__.py to traitlets/config/tests/__init__.py
NO CONTENT: file renamed from IPython/config/tests/__init__.py to traitlets/config/tests/__init__.py
@@ -1,199 +1,199 b''
1 # coding: utf-8
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 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 import logging
9 import logging
10 import os
10 import os
11 from io import StringIO
11 from io import StringIO
12 from unittest import TestCase
12 from unittest import TestCase
13
13
14 pjoin = os.path.join
14 pjoin = os.path.join
15
15
16 import nose.tools as nt
16 import nose.tools as nt
17
17
18 from IPython.config.configurable import Configurable
18 from traitlets.config.configurable import Configurable
19 from IPython.config.loader import Config
19 from traitlets.config.loader import Config
20
20
21 from IPython.config.application import (
21 from traitlets.config.application import (
22 Application
22 Application
23 )
23 )
24
24
25 from IPython.utils.tempdir import TemporaryDirectory
25 from IPython.utils.tempdir import TemporaryDirectory
26 from IPython.utils.traitlets import (
26 from traitlets.traitlets import (
27 Bool, Unicode, Integer, List, Dict
27 Bool, Unicode, Integer, List, Dict
28 )
28 )
29
29
30
30
31 class Foo(Configurable):
31 class Foo(Configurable):
32
32
33 i = Integer(0, config=True, help="The integer i.")
33 i = Integer(0, config=True, help="The integer i.")
34 j = Integer(1, config=True, help="The integer j.")
34 j = Integer(1, config=True, help="The integer j.")
35 name = Unicode(u'Brian', config=True, help="First name.")
35 name = Unicode(u'Brian', config=True, help="First name.")
36
36
37
37
38 class Bar(Configurable):
38 class Bar(Configurable):
39
39
40 b = Integer(0, config=True, help="The integer b.")
40 b = Integer(0, config=True, help="The integer b.")
41 enabled = Bool(True, config=True, help="Enable bar.")
41 enabled = Bool(True, config=True, help="Enable bar.")
42
42
43
43
44 class MyApp(Application):
44 class MyApp(Application):
45
45
46 name = Unicode(u'myapp')
46 name = Unicode(u'myapp')
47 running = Bool(False, config=True,
47 running = Bool(False, config=True,
48 help="Is the app running?")
48 help="Is the app running?")
49 classes = List([Bar, Foo])
49 classes = List([Bar, Foo])
50 config_file = Unicode(u'', config=True,
50 config_file = Unicode(u'', config=True,
51 help="Load this config file")
51 help="Load this config file")
52
52
53 aliases = Dict({
53 aliases = Dict({
54 'i' : 'Foo.i',
54 'i' : 'Foo.i',
55 'j' : 'Foo.j',
55 'j' : 'Foo.j',
56 'name' : 'Foo.name',
56 'name' : 'Foo.name',
57 'enabled' : 'Bar.enabled',
57 'enabled' : 'Bar.enabled',
58 'log-level' : 'Application.log_level',
58 'log-level' : 'Application.log_level',
59 })
59 })
60
60
61 flags = Dict(dict(enable=({'Bar': {'enabled' : True}}, "Set Bar.enabled to True"),
61 flags = Dict(dict(enable=({'Bar': {'enabled' : True}}, "Set Bar.enabled to True"),
62 disable=({'Bar': {'enabled' : False}}, "Set Bar.enabled to False"),
62 disable=({'Bar': {'enabled' : False}}, "Set Bar.enabled to False"),
63 crit=({'Application' : {'log_level' : logging.CRITICAL}},
63 crit=({'Application' : {'log_level' : logging.CRITICAL}},
64 "set level=CRITICAL"),
64 "set level=CRITICAL"),
65 ))
65 ))
66
66
67 def init_foo(self):
67 def init_foo(self):
68 self.foo = Foo(parent=self)
68 self.foo = Foo(parent=self)
69
69
70 def init_bar(self):
70 def init_bar(self):
71 self.bar = Bar(parent=self)
71 self.bar = Bar(parent=self)
72
72
73
73
74 class TestApplication(TestCase):
74 class TestApplication(TestCase):
75
75
76 def test_log(self):
76 def test_log(self):
77 stream = StringIO()
77 stream = StringIO()
78 app = MyApp(log_level=logging.INFO)
78 app = MyApp(log_level=logging.INFO)
79 handler = logging.StreamHandler(stream)
79 handler = logging.StreamHandler(stream)
80 # trigger reconstruction of the log formatter
80 # trigger reconstruction of the log formatter
81 app.log.handlers = [handler]
81 app.log.handlers = [handler]
82 app.log_format = "%(message)s"
82 app.log_format = "%(message)s"
83 app.log_datefmt = "%Y-%m-%d %H:%M"
83 app.log_datefmt = "%Y-%m-%d %H:%M"
84 app.log.info("hello")
84 app.log.info("hello")
85 nt.assert_in("hello", stream.getvalue())
85 nt.assert_in("hello", stream.getvalue())
86
86
87 def test_basic(self):
87 def test_basic(self):
88 app = MyApp()
88 app = MyApp()
89 self.assertEqual(app.name, u'myapp')
89 self.assertEqual(app.name, u'myapp')
90 self.assertEqual(app.running, False)
90 self.assertEqual(app.running, False)
91 self.assertEqual(app.classes, [MyApp,Bar,Foo])
91 self.assertEqual(app.classes, [MyApp,Bar,Foo])
92 self.assertEqual(app.config_file, u'')
92 self.assertEqual(app.config_file, u'')
93
93
94 def test_config(self):
94 def test_config(self):
95 app = MyApp()
95 app = MyApp()
96 app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
96 app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
97 config = app.config
97 config = app.config
98 self.assertEqual(config.Foo.i, 10)
98 self.assertEqual(config.Foo.i, 10)
99 self.assertEqual(config.Foo.j, 10)
99 self.assertEqual(config.Foo.j, 10)
100 self.assertEqual(config.Bar.enabled, False)
100 self.assertEqual(config.Bar.enabled, False)
101 self.assertEqual(config.MyApp.log_level,50)
101 self.assertEqual(config.MyApp.log_level,50)
102
102
103 def test_config_propagation(self):
103 def test_config_propagation(self):
104 app = MyApp()
104 app = MyApp()
105 app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
105 app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
106 app.init_foo()
106 app.init_foo()
107 app.init_bar()
107 app.init_bar()
108 self.assertEqual(app.foo.i, 10)
108 self.assertEqual(app.foo.i, 10)
109 self.assertEqual(app.foo.j, 10)
109 self.assertEqual(app.foo.j, 10)
110 self.assertEqual(app.bar.enabled, False)
110 self.assertEqual(app.bar.enabled, False)
111
111
112 def test_flags(self):
112 def test_flags(self):
113 app = MyApp()
113 app = MyApp()
114 app.parse_command_line(["--disable"])
114 app.parse_command_line(["--disable"])
115 app.init_bar()
115 app.init_bar()
116 self.assertEqual(app.bar.enabled, False)
116 self.assertEqual(app.bar.enabled, False)
117 app.parse_command_line(["--enable"])
117 app.parse_command_line(["--enable"])
118 app.init_bar()
118 app.init_bar()
119 self.assertEqual(app.bar.enabled, True)
119 self.assertEqual(app.bar.enabled, True)
120
120
121 def test_aliases(self):
121 def test_aliases(self):
122 app = MyApp()
122 app = MyApp()
123 app.parse_command_line(["--i=5", "--j=10"])
123 app.parse_command_line(["--i=5", "--j=10"])
124 app.init_foo()
124 app.init_foo()
125 self.assertEqual(app.foo.i, 5)
125 self.assertEqual(app.foo.i, 5)
126 app.init_foo()
126 app.init_foo()
127 self.assertEqual(app.foo.j, 10)
127 self.assertEqual(app.foo.j, 10)
128
128
129 def test_flag_clobber(self):
129 def test_flag_clobber(self):
130 """test that setting flags doesn't clobber existing settings"""
130 """test that setting flags doesn't clobber existing settings"""
131 app = MyApp()
131 app = MyApp()
132 app.parse_command_line(["--Bar.b=5", "--disable"])
132 app.parse_command_line(["--Bar.b=5", "--disable"])
133 app.init_bar()
133 app.init_bar()
134 self.assertEqual(app.bar.enabled, False)
134 self.assertEqual(app.bar.enabled, False)
135 self.assertEqual(app.bar.b, 5)
135 self.assertEqual(app.bar.b, 5)
136 app.parse_command_line(["--enable", "--Bar.b=10"])
136 app.parse_command_line(["--enable", "--Bar.b=10"])
137 app.init_bar()
137 app.init_bar()
138 self.assertEqual(app.bar.enabled, True)
138 self.assertEqual(app.bar.enabled, True)
139 self.assertEqual(app.bar.b, 10)
139 self.assertEqual(app.bar.b, 10)
140
140
141 def test_flatten_flags(self):
141 def test_flatten_flags(self):
142 cfg = Config()
142 cfg = Config()
143 cfg.MyApp.log_level = logging.WARN
143 cfg.MyApp.log_level = logging.WARN
144 app = MyApp()
144 app = MyApp()
145 app.update_config(cfg)
145 app.update_config(cfg)
146 self.assertEqual(app.log_level, logging.WARN)
146 self.assertEqual(app.log_level, logging.WARN)
147 self.assertEqual(app.config.MyApp.log_level, logging.WARN)
147 self.assertEqual(app.config.MyApp.log_level, logging.WARN)
148 app.initialize(["--crit"])
148 app.initialize(["--crit"])
149 self.assertEqual(app.log_level, logging.CRITICAL)
149 self.assertEqual(app.log_level, logging.CRITICAL)
150 # this would be app.config.Application.log_level if it failed:
150 # this would be app.config.Application.log_level if it failed:
151 self.assertEqual(app.config.MyApp.log_level, logging.CRITICAL)
151 self.assertEqual(app.config.MyApp.log_level, logging.CRITICAL)
152
152
153 def test_flatten_aliases(self):
153 def test_flatten_aliases(self):
154 cfg = Config()
154 cfg = Config()
155 cfg.MyApp.log_level = logging.WARN
155 cfg.MyApp.log_level = logging.WARN
156 app = MyApp()
156 app = MyApp()
157 app.update_config(cfg)
157 app.update_config(cfg)
158 self.assertEqual(app.log_level, logging.WARN)
158 self.assertEqual(app.log_level, logging.WARN)
159 self.assertEqual(app.config.MyApp.log_level, logging.WARN)
159 self.assertEqual(app.config.MyApp.log_level, logging.WARN)
160 app.initialize(["--log-level", "CRITICAL"])
160 app.initialize(["--log-level", "CRITICAL"])
161 self.assertEqual(app.log_level, logging.CRITICAL)
161 self.assertEqual(app.log_level, logging.CRITICAL)
162 # this would be app.config.Application.log_level if it failed:
162 # this would be app.config.Application.log_level if it failed:
163 self.assertEqual(app.config.MyApp.log_level, "CRITICAL")
163 self.assertEqual(app.config.MyApp.log_level, "CRITICAL")
164
164
165 def test_extra_args(self):
165 def test_extra_args(self):
166 app = MyApp()
166 app = MyApp()
167 app.parse_command_line(["--Bar.b=5", 'extra', "--disable", 'args'])
167 app.parse_command_line(["--Bar.b=5", 'extra', "--disable", 'args'])
168 app.init_bar()
168 app.init_bar()
169 self.assertEqual(app.bar.enabled, False)
169 self.assertEqual(app.bar.enabled, False)
170 self.assertEqual(app.bar.b, 5)
170 self.assertEqual(app.bar.b, 5)
171 self.assertEqual(app.extra_args, ['extra', 'args'])
171 self.assertEqual(app.extra_args, ['extra', 'args'])
172 app = MyApp()
172 app = MyApp()
173 app.parse_command_line(["--Bar.b=5", '--', 'extra', "--disable", 'args'])
173 app.parse_command_line(["--Bar.b=5", '--', 'extra', "--disable", 'args'])
174 app.init_bar()
174 app.init_bar()
175 self.assertEqual(app.bar.enabled, True)
175 self.assertEqual(app.bar.enabled, True)
176 self.assertEqual(app.bar.b, 5)
176 self.assertEqual(app.bar.b, 5)
177 self.assertEqual(app.extra_args, ['extra', '--disable', 'args'])
177 self.assertEqual(app.extra_args, ['extra', '--disable', 'args'])
178
178
179 def test_unicode_argv(self):
179 def test_unicode_argv(self):
180 app = MyApp()
180 app = MyApp()
181 app.parse_command_line(['ünîcødé'])
181 app.parse_command_line(['ünîcødé'])
182
182
183 def test_multi_file(self):
183 def test_multi_file(self):
184 app = MyApp()
184 app = MyApp()
185 app.log = logging.getLogger()
185 app.log = logging.getLogger()
186 name = 'config.py'
186 name = 'config.py'
187 with TemporaryDirectory('_1') as td1:
187 with TemporaryDirectory('_1') as td1:
188 with open(pjoin(td1, name), 'w') as f1:
188 with open(pjoin(td1, name), 'w') as f1:
189 f1.write("get_config().MyApp.Bar.b = 1")
189 f1.write("get_config().MyApp.Bar.b = 1")
190 with TemporaryDirectory('_2') as td2:
190 with TemporaryDirectory('_2') as td2:
191 with open(pjoin(td2, name), 'w') as f2:
191 with open(pjoin(td2, name), 'w') as f2:
192 f2.write("get_config().MyApp.Bar.b = 2")
192 f2.write("get_config().MyApp.Bar.b = 2")
193 app.load_config_file(name, path=[td2, td1])
193 app.load_config_file(name, path=[td2, td1])
194 app.init_bar()
194 app.init_bar()
195 self.assertEqual(app.bar.b, 2)
195 self.assertEqual(app.bar.b, 2)
196 app.load_config_file(name, path=[td1, td2])
196 app.load_config_file(name, path=[td1, td2])
197 app.init_bar()
197 app.init_bar()
198 self.assertEqual(app.bar.b, 1)
198 self.assertEqual(app.bar.b, 1)
199
199
@@ -1,378 +1,378 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """Tests for IPython.config.configurable"""
2 """Tests for traitlets.config.configurable"""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from unittest import TestCase
7 from unittest import TestCase
8
8
9 from IPython.config.configurable import (
9 from traitlets.config.configurable import (
10 Configurable,
10 Configurable,
11 SingletonConfigurable
11 SingletonConfigurable
12 )
12 )
13
13
14 from IPython.utils.traitlets import (
14 from traitlets.traitlets import (
15 Integer, Float, Unicode, List, Dict, Set,
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 from IPython.utils.py3compat import PY3
19 from IPython.utils.py3compat import PY3
20
20
21
21
22 class MyConfigurable(Configurable):
22 class MyConfigurable(Configurable):
23 a = Integer(1, config=True, help="The integer a.")
23 a = Integer(1, config=True, help="The integer a.")
24 b = Float(1.0, config=True, help="The integer b.")
24 b = Float(1.0, config=True, help="The integer b.")
25 c = Unicode('no config')
25 c = Unicode('no config')
26
26
27
27
28 mc_help=u"""MyConfigurable options
28 mc_help=u"""MyConfigurable options
29 ----------------------
29 ----------------------
30 --MyConfigurable.a=<Integer>
30 --MyConfigurable.a=<Integer>
31 Default: 1
31 Default: 1
32 The integer a.
32 The integer a.
33 --MyConfigurable.b=<Float>
33 --MyConfigurable.b=<Float>
34 Default: 1.0
34 Default: 1.0
35 The integer b."""
35 The integer b."""
36
36
37 mc_help_inst=u"""MyConfigurable options
37 mc_help_inst=u"""MyConfigurable options
38 ----------------------
38 ----------------------
39 --MyConfigurable.a=<Integer>
39 --MyConfigurable.a=<Integer>
40 Current: 5
40 Current: 5
41 The integer a.
41 The integer a.
42 --MyConfigurable.b=<Float>
42 --MyConfigurable.b=<Float>
43 Current: 4.0
43 Current: 4.0
44 The integer b."""
44 The integer b."""
45
45
46 # On Python 3, the Integer trait is a synonym for Int
46 # On Python 3, the Integer trait is a synonym for Int
47 if PY3:
47 if PY3:
48 mc_help = mc_help.replace(u"<Integer>", u"<Int>")
48 mc_help = mc_help.replace(u"<Integer>", u"<Int>")
49 mc_help_inst = mc_help_inst.replace(u"<Integer>", u"<Int>")
49 mc_help_inst = mc_help_inst.replace(u"<Integer>", u"<Int>")
50
50
51 class Foo(Configurable):
51 class Foo(Configurable):
52 a = Integer(0, config=True, help="The integer a.")
52 a = Integer(0, config=True, help="The integer a.")
53 b = Unicode('nope', config=True)
53 b = Unicode('nope', config=True)
54
54
55
55
56 class Bar(Foo):
56 class Bar(Foo):
57 b = Unicode('gotit', config=False, help="The string b.")
57 b = Unicode('gotit', config=False, help="The string b.")
58 c = Float(config=True, help="The string c.")
58 c = Float(config=True, help="The string c.")
59
59
60
60
61 class TestConfigurable(TestCase):
61 class TestConfigurable(TestCase):
62
62
63 def test_default(self):
63 def test_default(self):
64 c1 = Configurable()
64 c1 = Configurable()
65 c2 = Configurable(config=c1.config)
65 c2 = Configurable(config=c1.config)
66 c3 = Configurable(config=c2.config)
66 c3 = Configurable(config=c2.config)
67 self.assertEqual(c1.config, c2.config)
67 self.assertEqual(c1.config, c2.config)
68 self.assertEqual(c2.config, c3.config)
68 self.assertEqual(c2.config, c3.config)
69
69
70 def test_custom(self):
70 def test_custom(self):
71 config = Config()
71 config = Config()
72 config.foo = 'foo'
72 config.foo = 'foo'
73 config.bar = 'bar'
73 config.bar = 'bar'
74 c1 = Configurable(config=config)
74 c1 = Configurable(config=config)
75 c2 = Configurable(config=c1.config)
75 c2 = Configurable(config=c1.config)
76 c3 = Configurable(config=c2.config)
76 c3 = Configurable(config=c2.config)
77 self.assertEqual(c1.config, config)
77 self.assertEqual(c1.config, config)
78 self.assertEqual(c2.config, config)
78 self.assertEqual(c2.config, config)
79 self.assertEqual(c3.config, config)
79 self.assertEqual(c3.config, config)
80 # Test that copies are not made
80 # Test that copies are not made
81 self.assertTrue(c1.config is config)
81 self.assertTrue(c1.config is config)
82 self.assertTrue(c2.config is config)
82 self.assertTrue(c2.config is config)
83 self.assertTrue(c3.config is config)
83 self.assertTrue(c3.config is config)
84 self.assertTrue(c1.config is c2.config)
84 self.assertTrue(c1.config is c2.config)
85 self.assertTrue(c2.config is c3.config)
85 self.assertTrue(c2.config is c3.config)
86
86
87 def test_inheritance(self):
87 def test_inheritance(self):
88 config = Config()
88 config = Config()
89 config.MyConfigurable.a = 2
89 config.MyConfigurable.a = 2
90 config.MyConfigurable.b = 2.0
90 config.MyConfigurable.b = 2.0
91 c1 = MyConfigurable(config=config)
91 c1 = MyConfigurable(config=config)
92 c2 = MyConfigurable(config=c1.config)
92 c2 = MyConfigurable(config=c1.config)
93 self.assertEqual(c1.a, config.MyConfigurable.a)
93 self.assertEqual(c1.a, config.MyConfigurable.a)
94 self.assertEqual(c1.b, config.MyConfigurable.b)
94 self.assertEqual(c1.b, config.MyConfigurable.b)
95 self.assertEqual(c2.a, config.MyConfigurable.a)
95 self.assertEqual(c2.a, config.MyConfigurable.a)
96 self.assertEqual(c2.b, config.MyConfigurable.b)
96 self.assertEqual(c2.b, config.MyConfigurable.b)
97
97
98 def test_parent(self):
98 def test_parent(self):
99 config = Config()
99 config = Config()
100 config.Foo.a = 10
100 config.Foo.a = 10
101 config.Foo.b = "wow"
101 config.Foo.b = "wow"
102 config.Bar.b = 'later'
102 config.Bar.b = 'later'
103 config.Bar.c = 100.0
103 config.Bar.c = 100.0
104 f = Foo(config=config)
104 f = Foo(config=config)
105 b = Bar(config=f.config)
105 b = Bar(config=f.config)
106 self.assertEqual(f.a, 10)
106 self.assertEqual(f.a, 10)
107 self.assertEqual(f.b, 'wow')
107 self.assertEqual(f.b, 'wow')
108 self.assertEqual(b.b, 'gotit')
108 self.assertEqual(b.b, 'gotit')
109 self.assertEqual(b.c, 100.0)
109 self.assertEqual(b.c, 100.0)
110
110
111 def test_override1(self):
111 def test_override1(self):
112 config = Config()
112 config = Config()
113 config.MyConfigurable.a = 2
113 config.MyConfigurable.a = 2
114 config.MyConfigurable.b = 2.0
114 config.MyConfigurable.b = 2.0
115 c = MyConfigurable(a=3, config=config)
115 c = MyConfigurable(a=3, config=config)
116 self.assertEqual(c.a, 3)
116 self.assertEqual(c.a, 3)
117 self.assertEqual(c.b, config.MyConfigurable.b)
117 self.assertEqual(c.b, config.MyConfigurable.b)
118 self.assertEqual(c.c, 'no config')
118 self.assertEqual(c.c, 'no config')
119
119
120 def test_override2(self):
120 def test_override2(self):
121 config = Config()
121 config = Config()
122 config.Foo.a = 1
122 config.Foo.a = 1
123 config.Bar.b = 'or' # Up above b is config=False, so this won't do it.
123 config.Bar.b = 'or' # Up above b is config=False, so this won't do it.
124 config.Bar.c = 10.0
124 config.Bar.c = 10.0
125 c = Bar(config=config)
125 c = Bar(config=config)
126 self.assertEqual(c.a, config.Foo.a)
126 self.assertEqual(c.a, config.Foo.a)
127 self.assertEqual(c.b, 'gotit')
127 self.assertEqual(c.b, 'gotit')
128 self.assertEqual(c.c, config.Bar.c)
128 self.assertEqual(c.c, config.Bar.c)
129 c = Bar(a=2, b='and', c=20.0, config=config)
129 c = Bar(a=2, b='and', c=20.0, config=config)
130 self.assertEqual(c.a, 2)
130 self.assertEqual(c.a, 2)
131 self.assertEqual(c.b, 'and')
131 self.assertEqual(c.b, 'and')
132 self.assertEqual(c.c, 20.0)
132 self.assertEqual(c.c, 20.0)
133
133
134 def test_help(self):
134 def test_help(self):
135 self.assertEqual(MyConfigurable.class_get_help(), mc_help)
135 self.assertEqual(MyConfigurable.class_get_help(), mc_help)
136
136
137 def test_help_inst(self):
137 def test_help_inst(self):
138 inst = MyConfigurable(a=5, b=4)
138 inst = MyConfigurable(a=5, b=4)
139 self.assertEqual(MyConfigurable.class_get_help(inst), mc_help_inst)
139 self.assertEqual(MyConfigurable.class_get_help(inst), mc_help_inst)
140
140
141
141
142 class TestSingletonConfigurable(TestCase):
142 class TestSingletonConfigurable(TestCase):
143
143
144 def test_instance(self):
144 def test_instance(self):
145 class Foo(SingletonConfigurable): pass
145 class Foo(SingletonConfigurable): pass
146 self.assertEqual(Foo.initialized(), False)
146 self.assertEqual(Foo.initialized(), False)
147 foo = Foo.instance()
147 foo = Foo.instance()
148 self.assertEqual(Foo.initialized(), True)
148 self.assertEqual(Foo.initialized(), True)
149 self.assertEqual(foo, Foo.instance())
149 self.assertEqual(foo, Foo.instance())
150 self.assertEqual(SingletonConfigurable._instance, None)
150 self.assertEqual(SingletonConfigurable._instance, None)
151
151
152 def test_inheritance(self):
152 def test_inheritance(self):
153 class Bar(SingletonConfigurable): pass
153 class Bar(SingletonConfigurable): pass
154 class Bam(Bar): pass
154 class Bam(Bar): pass
155 self.assertEqual(Bar.initialized(), False)
155 self.assertEqual(Bar.initialized(), False)
156 self.assertEqual(Bam.initialized(), False)
156 self.assertEqual(Bam.initialized(), False)
157 bam = Bam.instance()
157 bam = Bam.instance()
158 bam == Bar.instance()
158 bam == Bar.instance()
159 self.assertEqual(Bar.initialized(), True)
159 self.assertEqual(Bar.initialized(), True)
160 self.assertEqual(Bam.initialized(), True)
160 self.assertEqual(Bam.initialized(), True)
161 self.assertEqual(bam, Bam._instance)
161 self.assertEqual(bam, Bam._instance)
162 self.assertEqual(bam, Bar._instance)
162 self.assertEqual(bam, Bar._instance)
163 self.assertEqual(SingletonConfigurable._instance, None)
163 self.assertEqual(SingletonConfigurable._instance, None)
164
164
165
165
166 class MyParent(Configurable):
166 class MyParent(Configurable):
167 pass
167 pass
168
168
169 class MyParent2(MyParent):
169 class MyParent2(MyParent):
170 pass
170 pass
171
171
172 class TestParentConfigurable(TestCase):
172 class TestParentConfigurable(TestCase):
173
173
174 def test_parent_config(self):
174 def test_parent_config(self):
175 cfg = Config({
175 cfg = Config({
176 'MyParent' : {
176 'MyParent' : {
177 'MyConfigurable' : {
177 'MyConfigurable' : {
178 'b' : 2.0,
178 'b' : 2.0,
179 }
179 }
180 }
180 }
181 })
181 })
182 parent = MyParent(config=cfg)
182 parent = MyParent(config=cfg)
183 myc = MyConfigurable(parent=parent)
183 myc = MyConfigurable(parent=parent)
184 self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)
184 self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)
185
185
186 def test_parent_inheritance(self):
186 def test_parent_inheritance(self):
187 cfg = Config({
187 cfg = Config({
188 'MyParent' : {
188 'MyParent' : {
189 'MyConfigurable' : {
189 'MyConfigurable' : {
190 'b' : 2.0,
190 'b' : 2.0,
191 }
191 }
192 }
192 }
193 })
193 })
194 parent = MyParent2(config=cfg)
194 parent = MyParent2(config=cfg)
195 myc = MyConfigurable(parent=parent)
195 myc = MyConfigurable(parent=parent)
196 self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)
196 self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)
197
197
198 def test_multi_parent(self):
198 def test_multi_parent(self):
199 cfg = Config({
199 cfg = Config({
200 'MyParent2' : {
200 'MyParent2' : {
201 'MyParent' : {
201 'MyParent' : {
202 'MyConfigurable' : {
202 'MyConfigurable' : {
203 'b' : 2.0,
203 'b' : 2.0,
204 }
204 }
205 },
205 },
206 # this one shouldn't count
206 # this one shouldn't count
207 'MyConfigurable' : {
207 'MyConfigurable' : {
208 'b' : 3.0,
208 'b' : 3.0,
209 },
209 },
210 }
210 }
211 })
211 })
212 parent2 = MyParent2(config=cfg)
212 parent2 = MyParent2(config=cfg)
213 parent = MyParent(parent=parent2)
213 parent = MyParent(parent=parent2)
214 myc = MyConfigurable(parent=parent)
214 myc = MyConfigurable(parent=parent)
215 self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b)
215 self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b)
216
216
217 def test_parent_priority(self):
217 def test_parent_priority(self):
218 cfg = Config({
218 cfg = Config({
219 'MyConfigurable' : {
219 'MyConfigurable' : {
220 'b' : 2.0,
220 'b' : 2.0,
221 },
221 },
222 'MyParent' : {
222 'MyParent' : {
223 'MyConfigurable' : {
223 'MyConfigurable' : {
224 'b' : 3.0,
224 'b' : 3.0,
225 }
225 }
226 },
226 },
227 'MyParent2' : {
227 'MyParent2' : {
228 'MyConfigurable' : {
228 'MyConfigurable' : {
229 'b' : 4.0,
229 'b' : 4.0,
230 }
230 }
231 }
231 }
232 })
232 })
233 parent = MyParent2(config=cfg)
233 parent = MyParent2(config=cfg)
234 myc = MyConfigurable(parent=parent)
234 myc = MyConfigurable(parent=parent)
235 self.assertEqual(myc.b, parent.config.MyParent2.MyConfigurable.b)
235 self.assertEqual(myc.b, parent.config.MyParent2.MyConfigurable.b)
236
236
237 def test_multi_parent_priority(self):
237 def test_multi_parent_priority(self):
238 cfg = Config({
238 cfg = Config({
239 'MyConfigurable' : {
239 'MyConfigurable' : {
240 'b' : 2.0,
240 'b' : 2.0,
241 },
241 },
242 'MyParent' : {
242 'MyParent' : {
243 'MyConfigurable' : {
243 'MyConfigurable' : {
244 'b' : 3.0,
244 'b' : 3.0,
245 }
245 }
246 },
246 },
247 'MyParent2' : {
247 'MyParent2' : {
248 'MyConfigurable' : {
248 'MyConfigurable' : {
249 'b' : 4.0,
249 'b' : 4.0,
250 }
250 }
251 },
251 },
252 'MyParent2' : {
252 'MyParent2' : {
253 'MyParent' : {
253 'MyParent' : {
254 'MyConfigurable' : {
254 'MyConfigurable' : {
255 'b' : 5.0,
255 'b' : 5.0,
256 }
256 }
257 }
257 }
258 }
258 }
259 })
259 })
260 parent2 = MyParent2(config=cfg)
260 parent2 = MyParent2(config=cfg)
261 parent = MyParent2(parent=parent2)
261 parent = MyParent2(parent=parent2)
262 myc = MyConfigurable(parent=parent)
262 myc = MyConfigurable(parent=parent)
263 self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b)
263 self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b)
264
264
265 class Containers(Configurable):
265 class Containers(Configurable):
266 lis = List(config=True)
266 lis = List(config=True)
267 def _lis_default(self):
267 def _lis_default(self):
268 return [-1]
268 return [-1]
269
269
270 s = Set(config=True)
270 s = Set(config=True)
271 def _s_default(self):
271 def _s_default(self):
272 return {'a'}
272 return {'a'}
273
273
274 d = Dict(config=True)
274 d = Dict(config=True)
275 def _d_default(self):
275 def _d_default(self):
276 return {'a' : 'b'}
276 return {'a' : 'b'}
277
277
278 class TestConfigContainers(TestCase):
278 class TestConfigContainers(TestCase):
279 def test_extend(self):
279 def test_extend(self):
280 c = Config()
280 c = Config()
281 c.Containers.lis.extend(list(range(5)))
281 c.Containers.lis.extend(list(range(5)))
282 obj = Containers(config=c)
282 obj = Containers(config=c)
283 self.assertEqual(obj.lis, list(range(-1,5)))
283 self.assertEqual(obj.lis, list(range(-1,5)))
284
284
285 def test_insert(self):
285 def test_insert(self):
286 c = Config()
286 c = Config()
287 c.Containers.lis.insert(0, 'a')
287 c.Containers.lis.insert(0, 'a')
288 c.Containers.lis.insert(1, 'b')
288 c.Containers.lis.insert(1, 'b')
289 obj = Containers(config=c)
289 obj = Containers(config=c)
290 self.assertEqual(obj.lis, ['a', 'b', -1])
290 self.assertEqual(obj.lis, ['a', 'b', -1])
291
291
292 def test_prepend(self):
292 def test_prepend(self):
293 c = Config()
293 c = Config()
294 c.Containers.lis.prepend([1,2])
294 c.Containers.lis.prepend([1,2])
295 c.Containers.lis.prepend([2,3])
295 c.Containers.lis.prepend([2,3])
296 obj = Containers(config=c)
296 obj = Containers(config=c)
297 self.assertEqual(obj.lis, [2,3,1,2,-1])
297 self.assertEqual(obj.lis, [2,3,1,2,-1])
298
298
299 def test_prepend_extend(self):
299 def test_prepend_extend(self):
300 c = Config()
300 c = Config()
301 c.Containers.lis.prepend([1,2])
301 c.Containers.lis.prepend([1,2])
302 c.Containers.lis.extend([2,3])
302 c.Containers.lis.extend([2,3])
303 obj = Containers(config=c)
303 obj = Containers(config=c)
304 self.assertEqual(obj.lis, [1,2,-1,2,3])
304 self.assertEqual(obj.lis, [1,2,-1,2,3])
305
305
306 def test_append_extend(self):
306 def test_append_extend(self):
307 c = Config()
307 c = Config()
308 c.Containers.lis.append([1,2])
308 c.Containers.lis.append([1,2])
309 c.Containers.lis.extend([2,3])
309 c.Containers.lis.extend([2,3])
310 obj = Containers(config=c)
310 obj = Containers(config=c)
311 self.assertEqual(obj.lis, [-1,[1,2],2,3])
311 self.assertEqual(obj.lis, [-1,[1,2],2,3])
312
312
313 def test_extend_append(self):
313 def test_extend_append(self):
314 c = Config()
314 c = Config()
315 c.Containers.lis.extend([2,3])
315 c.Containers.lis.extend([2,3])
316 c.Containers.lis.append([1,2])
316 c.Containers.lis.append([1,2])
317 obj = Containers(config=c)
317 obj = Containers(config=c)
318 self.assertEqual(obj.lis, [-1,2,3,[1,2]])
318 self.assertEqual(obj.lis, [-1,2,3,[1,2]])
319
319
320 def test_insert_extend(self):
320 def test_insert_extend(self):
321 c = Config()
321 c = Config()
322 c.Containers.lis.insert(0, 1)
322 c.Containers.lis.insert(0, 1)
323 c.Containers.lis.extend([2,3])
323 c.Containers.lis.extend([2,3])
324 obj = Containers(config=c)
324 obj = Containers(config=c)
325 self.assertEqual(obj.lis, [1,-1,2,3])
325 self.assertEqual(obj.lis, [1,-1,2,3])
326
326
327 def test_set_update(self):
327 def test_set_update(self):
328 c = Config()
328 c = Config()
329 c.Containers.s.update({0,1,2})
329 c.Containers.s.update({0,1,2})
330 c.Containers.s.update({3})
330 c.Containers.s.update({3})
331 obj = Containers(config=c)
331 obj = Containers(config=c)
332 self.assertEqual(obj.s, {'a', 0, 1, 2, 3})
332 self.assertEqual(obj.s, {'a', 0, 1, 2, 3})
333
333
334 def test_dict_update(self):
334 def test_dict_update(self):
335 c = Config()
335 c = Config()
336 c.Containers.d.update({'c' : 'd'})
336 c.Containers.d.update({'c' : 'd'})
337 c.Containers.d.update({'e' : 'f'})
337 c.Containers.d.update({'e' : 'f'})
338 obj = Containers(config=c)
338 obj = Containers(config=c)
339 self.assertEqual(obj.d, {'a':'b', 'c':'d', 'e':'f'})
339 self.assertEqual(obj.d, {'a':'b', 'c':'d', 'e':'f'})
340
340
341 def test_update_twice(self):
341 def test_update_twice(self):
342 c = Config()
342 c = Config()
343 c.MyConfigurable.a = 5
343 c.MyConfigurable.a = 5
344 m = MyConfigurable(config=c)
344 m = MyConfigurable(config=c)
345 self.assertEqual(m.a, 5)
345 self.assertEqual(m.a, 5)
346
346
347 c2 = Config()
347 c2 = Config()
348 c2.MyConfigurable.a = 10
348 c2.MyConfigurable.a = 10
349 m.update_config(c2)
349 m.update_config(c2)
350 self.assertEqual(m.a, 10)
350 self.assertEqual(m.a, 10)
351
351
352 c2.MyConfigurable.a = 15
352 c2.MyConfigurable.a = 15
353 m.update_config(c2)
353 m.update_config(c2)
354 self.assertEqual(m.a, 15)
354 self.assertEqual(m.a, 15)
355
355
356 def test_config_default(self):
356 def test_config_default(self):
357 class SomeSingleton(SingletonConfigurable):
357 class SomeSingleton(SingletonConfigurable):
358 pass
358 pass
359
359
360 class DefaultConfigurable(Configurable):
360 class DefaultConfigurable(Configurable):
361 a = Integer(config=True)
361 a = Integer(config=True)
362 def _config_default(self):
362 def _config_default(self):
363 if SomeSingleton.initialized():
363 if SomeSingleton.initialized():
364 return SomeSingleton.instance().config
364 return SomeSingleton.instance().config
365 return Config()
365 return Config()
366
366
367 c = Config()
367 c = Config()
368 c.DefaultConfigurable.a = 5
368 c.DefaultConfigurable.a = 5
369
369
370 d1 = DefaultConfigurable()
370 d1 = DefaultConfigurable()
371 self.assertEqual(d1.a, 0)
371 self.assertEqual(d1.a, 0)
372
372
373 single = SomeSingleton.instance(config=c)
373 single = SomeSingleton.instance(config=c)
374
374
375 d2 = DefaultConfigurable()
375 d2 = DefaultConfigurable()
376 self.assertIs(d2.config, single.config)
376 self.assertIs(d2.config, single.config)
377 self.assertEqual(d2.a, 5)
377 self.assertEqual(d2.a, 5)
378
378
@@ -1,419 +1,419 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """Tests for IPython.config.loader"""
2 """Tests for traitlets.config.loader"""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 import copy
7 import copy
8 import logging
8 import logging
9 import os
9 import os
10 import pickle
10 import pickle
11 import sys
11 import sys
12
12
13 from tempfile import mkstemp
13 from tempfile import mkstemp
14 from unittest import TestCase
14 from unittest import TestCase
15
15
16 from nose import SkipTest
16 from nose import SkipTest
17 import nose.tools as nt
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 Config,
22 Config,
23 LazyConfigValue,
23 LazyConfigValue,
24 PyFileConfigLoader,
24 PyFileConfigLoader,
25 JSONFileConfigLoader,
25 JSONFileConfigLoader,
26 KeyValueConfigLoader,
26 KeyValueConfigLoader,
27 ArgParseConfigLoader,
27 ArgParseConfigLoader,
28 KVArgParseConfigLoader,
28 KVArgParseConfigLoader,
29 ConfigError,
29 ConfigError,
30 )
30 )
31
31
32
32
33 pyfile = """
33 pyfile = """
34 c = get_config()
34 c = get_config()
35 c.a=10
35 c.a=10
36 c.b=20
36 c.b=20
37 c.Foo.Bar.value=10
37 c.Foo.Bar.value=10
38 c.Foo.Bam.value=list(range(10))
38 c.Foo.Bam.value=list(range(10))
39 c.D.C.value='hi there'
39 c.D.C.value='hi there'
40 """
40 """
41
41
42 json1file = """
42 json1file = """
43 {
43 {
44 "version": 1,
44 "version": 1,
45 "a": 10,
45 "a": 10,
46 "b": 20,
46 "b": 20,
47 "Foo": {
47 "Foo": {
48 "Bam": {
48 "Bam": {
49 "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
49 "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
50 },
50 },
51 "Bar": {
51 "Bar": {
52 "value": 10
52 "value": 10
53 }
53 }
54 },
54 },
55 "D": {
55 "D": {
56 "C": {
56 "C": {
57 "value": "hi there"
57 "value": "hi there"
58 }
58 }
59 }
59 }
60 }
60 }
61 """
61 """
62
62
63 # should not load
63 # should not load
64 json2file = """
64 json2file = """
65 {
65 {
66 "version": 2
66 "version": 2
67 }
67 }
68 """
68 """
69
69
70 import logging
70 import logging
71 log = logging.getLogger('devnull')
71 log = logging.getLogger('devnull')
72 log.setLevel(0)
72 log.setLevel(0)
73
73
74 class TestFileCL(TestCase):
74 class TestFileCL(TestCase):
75
75
76 def _check_conf(self, config):
76 def _check_conf(self, config):
77 self.assertEqual(config.a, 10)
77 self.assertEqual(config.a, 10)
78 self.assertEqual(config.b, 20)
78 self.assertEqual(config.b, 20)
79 self.assertEqual(config.Foo.Bar.value, 10)
79 self.assertEqual(config.Foo.Bar.value, 10)
80 self.assertEqual(config.Foo.Bam.value, list(range(10)))
80 self.assertEqual(config.Foo.Bam.value, list(range(10)))
81 self.assertEqual(config.D.C.value, 'hi there')
81 self.assertEqual(config.D.C.value, 'hi there')
82
82
83 def test_python(self):
83 def test_python(self):
84 fd, fname = mkstemp('.py')
84 fd, fname = mkstemp('.py')
85 f = os.fdopen(fd, 'w')
85 f = os.fdopen(fd, 'w')
86 f.write(pyfile)
86 f.write(pyfile)
87 f.close()
87 f.close()
88 # Unlink the file
88 # Unlink the file
89 cl = PyFileConfigLoader(fname, log=log)
89 cl = PyFileConfigLoader(fname, log=log)
90 config = cl.load_config()
90 config = cl.load_config()
91 self._check_conf(config)
91 self._check_conf(config)
92
92
93 def test_json(self):
93 def test_json(self):
94 fd, fname = mkstemp('.json')
94 fd, fname = mkstemp('.json')
95 f = os.fdopen(fd, 'w')
95 f = os.fdopen(fd, 'w')
96 f.write(json1file)
96 f.write(json1file)
97 f.close()
97 f.close()
98 # Unlink the file
98 # Unlink the file
99 cl = JSONFileConfigLoader(fname, log=log)
99 cl = JSONFileConfigLoader(fname, log=log)
100 config = cl.load_config()
100 config = cl.load_config()
101 self._check_conf(config)
101 self._check_conf(config)
102
102
103 def test_collision(self):
103 def test_collision(self):
104 a = Config()
104 a = Config()
105 b = Config()
105 b = Config()
106 self.assertEqual(a.collisions(b), {})
106 self.assertEqual(a.collisions(b), {})
107 a.A.trait1 = 1
107 a.A.trait1 = 1
108 b.A.trait2 = 2
108 b.A.trait2 = 2
109 self.assertEqual(a.collisions(b), {})
109 self.assertEqual(a.collisions(b), {})
110 b.A.trait1 = 1
110 b.A.trait1 = 1
111 self.assertEqual(a.collisions(b), {})
111 self.assertEqual(a.collisions(b), {})
112 b.A.trait1 = 0
112 b.A.trait1 = 0
113 self.assertEqual(a.collisions(b), {
113 self.assertEqual(a.collisions(b), {
114 'A': {
114 'A': {
115 'trait1': "1 ignored, using 0",
115 'trait1': "1 ignored, using 0",
116 }
116 }
117 })
117 })
118 self.assertEqual(b.collisions(a), {
118 self.assertEqual(b.collisions(a), {
119 'A': {
119 'A': {
120 'trait1': "0 ignored, using 1",
120 'trait1': "0 ignored, using 1",
121 }
121 }
122 })
122 })
123 a.A.trait2 = 3
123 a.A.trait2 = 3
124 self.assertEqual(b.collisions(a), {
124 self.assertEqual(b.collisions(a), {
125 'A': {
125 'A': {
126 'trait1': "0 ignored, using 1",
126 'trait1': "0 ignored, using 1",
127 'trait2': "2 ignored, using 3",
127 'trait2': "2 ignored, using 3",
128 }
128 }
129 })
129 })
130
130
131 def test_v2raise(self):
131 def test_v2raise(self):
132 fd, fname = mkstemp('.json')
132 fd, fname = mkstemp('.json')
133 f = os.fdopen(fd, 'w')
133 f = os.fdopen(fd, 'w')
134 f.write(json2file)
134 f.write(json2file)
135 f.close()
135 f.close()
136 # Unlink the file
136 # Unlink the file
137 cl = JSONFileConfigLoader(fname, log=log)
137 cl = JSONFileConfigLoader(fname, log=log)
138 with nt.assert_raises(ValueError):
138 with nt.assert_raises(ValueError):
139 cl.load_config()
139 cl.load_config()
140
140
141
141
142 class MyLoader1(ArgParseConfigLoader):
142 class MyLoader1(ArgParseConfigLoader):
143 def _add_arguments(self, aliases=None, flags=None):
143 def _add_arguments(self, aliases=None, flags=None):
144 p = self.parser
144 p = self.parser
145 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
145 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
146 p.add_argument('-b', dest='MyClass.bar', type=int)
146 p.add_argument('-b', dest='MyClass.bar', type=int)
147 p.add_argument('-n', dest='n', action='store_true')
147 p.add_argument('-n', dest='n', action='store_true')
148 p.add_argument('Global.bam', type=str)
148 p.add_argument('Global.bam', type=str)
149
149
150 class MyLoader2(ArgParseConfigLoader):
150 class MyLoader2(ArgParseConfigLoader):
151 def _add_arguments(self, aliases=None, flags=None):
151 def _add_arguments(self, aliases=None, flags=None):
152 subparsers = self.parser.add_subparsers(dest='subparser_name')
152 subparsers = self.parser.add_subparsers(dest='subparser_name')
153 subparser1 = subparsers.add_parser('1')
153 subparser1 = subparsers.add_parser('1')
154 subparser1.add_argument('-x',dest='Global.x')
154 subparser1.add_argument('-x',dest='Global.x')
155 subparser2 = subparsers.add_parser('2')
155 subparser2 = subparsers.add_parser('2')
156 subparser2.add_argument('y')
156 subparser2.add_argument('y')
157
157
158 class TestArgParseCL(TestCase):
158 class TestArgParseCL(TestCase):
159
159
160 def test_basic(self):
160 def test_basic(self):
161 cl = MyLoader1()
161 cl = MyLoader1()
162 config = cl.load_config('-f hi -b 10 -n wow'.split())
162 config = cl.load_config('-f hi -b 10 -n wow'.split())
163 self.assertEqual(config.Global.foo, 'hi')
163 self.assertEqual(config.Global.foo, 'hi')
164 self.assertEqual(config.MyClass.bar, 10)
164 self.assertEqual(config.MyClass.bar, 10)
165 self.assertEqual(config.n, True)
165 self.assertEqual(config.n, True)
166 self.assertEqual(config.Global.bam, 'wow')
166 self.assertEqual(config.Global.bam, 'wow')
167 config = cl.load_config(['wow'])
167 config = cl.load_config(['wow'])
168 self.assertEqual(list(config.keys()), ['Global'])
168 self.assertEqual(list(config.keys()), ['Global'])
169 self.assertEqual(list(config.Global.keys()), ['bam'])
169 self.assertEqual(list(config.Global.keys()), ['bam'])
170 self.assertEqual(config.Global.bam, 'wow')
170 self.assertEqual(config.Global.bam, 'wow')
171
171
172 def test_add_arguments(self):
172 def test_add_arguments(self):
173 cl = MyLoader2()
173 cl = MyLoader2()
174 config = cl.load_config('2 frobble'.split())
174 config = cl.load_config('2 frobble'.split())
175 self.assertEqual(config.subparser_name, '2')
175 self.assertEqual(config.subparser_name, '2')
176 self.assertEqual(config.y, 'frobble')
176 self.assertEqual(config.y, 'frobble')
177 config = cl.load_config('1 -x frobble'.split())
177 config = cl.load_config('1 -x frobble'.split())
178 self.assertEqual(config.subparser_name, '1')
178 self.assertEqual(config.subparser_name, '1')
179 self.assertEqual(config.Global.x, 'frobble')
179 self.assertEqual(config.Global.x, 'frobble')
180
180
181 def test_argv(self):
181 def test_argv(self):
182 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
182 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
183 config = cl.load_config()
183 config = cl.load_config()
184 self.assertEqual(config.Global.foo, 'hi')
184 self.assertEqual(config.Global.foo, 'hi')
185 self.assertEqual(config.MyClass.bar, 10)
185 self.assertEqual(config.MyClass.bar, 10)
186 self.assertEqual(config.n, True)
186 self.assertEqual(config.n, True)
187 self.assertEqual(config.Global.bam, 'wow')
187 self.assertEqual(config.Global.bam, 'wow')
188
188
189
189
190 class TestKeyValueCL(TestCase):
190 class TestKeyValueCL(TestCase):
191 klass = KeyValueConfigLoader
191 klass = KeyValueConfigLoader
192
192
193 def test_eval(self):
193 def test_eval(self):
194 cl = self.klass(log=log)
194 cl = self.klass(log=log)
195 config = cl.load_config('--Class.str_trait=all --Class.int_trait=5 --Class.list_trait=["hello",5]'.split())
195 config = cl.load_config('--Class.str_trait=all --Class.int_trait=5 --Class.list_trait=["hello",5]'.split())
196 self.assertEqual(config.Class.str_trait, 'all')
196 self.assertEqual(config.Class.str_trait, 'all')
197 self.assertEqual(config.Class.int_trait, 5)
197 self.assertEqual(config.Class.int_trait, 5)
198 self.assertEqual(config.Class.list_trait, ["hello", 5])
198 self.assertEqual(config.Class.list_trait, ["hello", 5])
199
199
200 def test_basic(self):
200 def test_basic(self):
201 cl = self.klass(log=log)
201 cl = self.klass(log=log)
202 argv = [ '--' + s[2:] for s in pyfile.split('\n') if s.startswith('c.') ]
202 argv = [ '--' + s[2:] for s in pyfile.split('\n') if s.startswith('c.') ]
203 print(argv)
203 print(argv)
204 config = cl.load_config(argv)
204 config = cl.load_config(argv)
205 self.assertEqual(config.a, 10)
205 self.assertEqual(config.a, 10)
206 self.assertEqual(config.b, 20)
206 self.assertEqual(config.b, 20)
207 self.assertEqual(config.Foo.Bar.value, 10)
207 self.assertEqual(config.Foo.Bar.value, 10)
208 # non-literal expressions are not evaluated
208 # non-literal expressions are not evaluated
209 self.assertEqual(config.Foo.Bam.value, 'list(range(10))')
209 self.assertEqual(config.Foo.Bam.value, 'list(range(10))')
210 self.assertEqual(config.D.C.value, 'hi there')
210 self.assertEqual(config.D.C.value, 'hi there')
211
211
212 def test_expanduser(self):
212 def test_expanduser(self):
213 cl = self.klass(log=log)
213 cl = self.klass(log=log)
214 argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
214 argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
215 config = cl.load_config(argv)
215 config = cl.load_config(argv)
216 self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
216 self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
217 self.assertEqual(config.b, os.path.expanduser('~'))
217 self.assertEqual(config.b, os.path.expanduser('~'))
218 self.assertEqual(config.c, os.path.expanduser('~/'))
218 self.assertEqual(config.c, os.path.expanduser('~/'))
219 self.assertEqual(config.d, '~/')
219 self.assertEqual(config.d, '~/')
220
220
221 def test_extra_args(self):
221 def test_extra_args(self):
222 cl = self.klass(log=log)
222 cl = self.klass(log=log)
223 config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
223 config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
224 self.assertEqual(cl.extra_args, ['b', 'd'])
224 self.assertEqual(cl.extra_args, ['b', 'd'])
225 self.assertEqual(config.a, 5)
225 self.assertEqual(config.a, 5)
226 self.assertEqual(config.c, 10)
226 self.assertEqual(config.c, 10)
227 config = cl.load_config(['--', '--a=5', '--c=10'])
227 config = cl.load_config(['--', '--a=5', '--c=10'])
228 self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
228 self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
229
229
230 def test_unicode_args(self):
230 def test_unicode_args(self):
231 cl = self.klass(log=log)
231 cl = self.klass(log=log)
232 argv = [u'--a=épsîlön']
232 argv = [u'--a=épsîlön']
233 config = cl.load_config(argv)
233 config = cl.load_config(argv)
234 self.assertEqual(config.a, u'épsîlön')
234 self.assertEqual(config.a, u'épsîlön')
235
235
236 def test_unicode_bytes_args(self):
236 def test_unicode_bytes_args(self):
237 uarg = u'--a=é'
237 uarg = u'--a=é'
238 try:
238 try:
239 barg = uarg.encode(sys.stdin.encoding)
239 barg = uarg.encode(sys.stdin.encoding)
240 except (TypeError, UnicodeEncodeError):
240 except (TypeError, UnicodeEncodeError):
241 raise SkipTest("sys.stdin.encoding can't handle 'é'")
241 raise SkipTest("sys.stdin.encoding can't handle 'é'")
242
242
243 cl = self.klass(log=log)
243 cl = self.klass(log=log)
244 config = cl.load_config([barg])
244 config = cl.load_config([barg])
245 self.assertEqual(config.a, u'é')
245 self.assertEqual(config.a, u'é')
246
246
247 def test_unicode_alias(self):
247 def test_unicode_alias(self):
248 cl = self.klass(log=log)
248 cl = self.klass(log=log)
249 argv = [u'--a=épsîlön']
249 argv = [u'--a=épsîlön']
250 config = cl.load_config(argv, aliases=dict(a='A.a'))
250 config = cl.load_config(argv, aliases=dict(a='A.a'))
251 self.assertEqual(config.A.a, u'épsîlön')
251 self.assertEqual(config.A.a, u'épsîlön')
252
252
253
253
254 class TestArgParseKVCL(TestKeyValueCL):
254 class TestArgParseKVCL(TestKeyValueCL):
255 klass = KVArgParseConfigLoader
255 klass = KVArgParseConfigLoader
256
256
257 def test_expanduser2(self):
257 def test_expanduser2(self):
258 cl = self.klass(log=log)
258 cl = self.klass(log=log)
259 argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
259 argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
260 config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
260 config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
261 self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
261 self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
262 self.assertEqual(config.A.b, '~/1/2/3')
262 self.assertEqual(config.A.b, '~/1/2/3')
263
263
264 def test_eval(self):
264 def test_eval(self):
265 cl = self.klass(log=log)
265 cl = self.klass(log=log)
266 argv = ['-c', 'a=5']
266 argv = ['-c', 'a=5']
267 config = cl.load_config(argv, aliases=dict(c='A.c'))
267 config = cl.load_config(argv, aliases=dict(c='A.c'))
268 self.assertEqual(config.A.c, u"a=5")
268 self.assertEqual(config.A.c, u"a=5")
269
269
270
270
271 class TestConfig(TestCase):
271 class TestConfig(TestCase):
272
272
273 def test_setget(self):
273 def test_setget(self):
274 c = Config()
274 c = Config()
275 c.a = 10
275 c.a = 10
276 self.assertEqual(c.a, 10)
276 self.assertEqual(c.a, 10)
277 self.assertEqual('b' in c, False)
277 self.assertEqual('b' in c, False)
278
278
279 def test_auto_section(self):
279 def test_auto_section(self):
280 c = Config()
280 c = Config()
281 self.assertNotIn('A', c)
281 self.assertNotIn('A', c)
282 assert not c._has_section('A')
282 assert not c._has_section('A')
283 A = c.A
283 A = c.A
284 A.foo = 'hi there'
284 A.foo = 'hi there'
285 self.assertIn('A', c)
285 self.assertIn('A', c)
286 assert c._has_section('A')
286 assert c._has_section('A')
287 self.assertEqual(c.A.foo, 'hi there')
287 self.assertEqual(c.A.foo, 'hi there')
288 del c.A
288 del c.A
289 self.assertEqual(c.A, Config())
289 self.assertEqual(c.A, Config())
290
290
291 def test_merge_doesnt_exist(self):
291 def test_merge_doesnt_exist(self):
292 c1 = Config()
292 c1 = Config()
293 c2 = Config()
293 c2 = Config()
294 c2.bar = 10
294 c2.bar = 10
295 c2.Foo.bar = 10
295 c2.Foo.bar = 10
296 c1.merge(c2)
296 c1.merge(c2)
297 self.assertEqual(c1.Foo.bar, 10)
297 self.assertEqual(c1.Foo.bar, 10)
298 self.assertEqual(c1.bar, 10)
298 self.assertEqual(c1.bar, 10)
299 c2.Bar.bar = 10
299 c2.Bar.bar = 10
300 c1.merge(c2)
300 c1.merge(c2)
301 self.assertEqual(c1.Bar.bar, 10)
301 self.assertEqual(c1.Bar.bar, 10)
302
302
303 def test_merge_exists(self):
303 def test_merge_exists(self):
304 c1 = Config()
304 c1 = Config()
305 c2 = Config()
305 c2 = Config()
306 c1.Foo.bar = 10
306 c1.Foo.bar = 10
307 c1.Foo.bam = 30
307 c1.Foo.bam = 30
308 c2.Foo.bar = 20
308 c2.Foo.bar = 20
309 c2.Foo.wow = 40
309 c2.Foo.wow = 40
310 c1.merge(c2)
310 c1.merge(c2)
311 self.assertEqual(c1.Foo.bam, 30)
311 self.assertEqual(c1.Foo.bam, 30)
312 self.assertEqual(c1.Foo.bar, 20)
312 self.assertEqual(c1.Foo.bar, 20)
313 self.assertEqual(c1.Foo.wow, 40)
313 self.assertEqual(c1.Foo.wow, 40)
314 c2.Foo.Bam.bam = 10
314 c2.Foo.Bam.bam = 10
315 c1.merge(c2)
315 c1.merge(c2)
316 self.assertEqual(c1.Foo.Bam.bam, 10)
316 self.assertEqual(c1.Foo.Bam.bam, 10)
317
317
318 def test_deepcopy(self):
318 def test_deepcopy(self):
319 c1 = Config()
319 c1 = Config()
320 c1.Foo.bar = 10
320 c1.Foo.bar = 10
321 c1.Foo.bam = 30
321 c1.Foo.bam = 30
322 c1.a = 'asdf'
322 c1.a = 'asdf'
323 c1.b = range(10)
323 c1.b = range(10)
324 c1.Test.logger = logging.Logger('test')
324 c1.Test.logger = logging.Logger('test')
325 c1.Test.get_logger = logging.getLogger('test')
325 c1.Test.get_logger = logging.getLogger('test')
326 c2 = copy.deepcopy(c1)
326 c2 = copy.deepcopy(c1)
327 self.assertEqual(c1, c2)
327 self.assertEqual(c1, c2)
328 self.assertTrue(c1 is not c2)
328 self.assertTrue(c1 is not c2)
329 self.assertTrue(c1.Foo is not c2.Foo)
329 self.assertTrue(c1.Foo is not c2.Foo)
330 self.assertTrue(c1.Test is not c2.Test)
330 self.assertTrue(c1.Test is not c2.Test)
331 self.assertTrue(c1.Test.logger is c2.Test.logger)
331 self.assertTrue(c1.Test.logger is c2.Test.logger)
332 self.assertTrue(c1.Test.get_logger is c2.Test.get_logger)
332 self.assertTrue(c1.Test.get_logger is c2.Test.get_logger)
333
333
334 def test_builtin(self):
334 def test_builtin(self):
335 c1 = Config()
335 c1 = Config()
336 c1.format = "json"
336 c1.format = "json"
337
337
338 def test_fromdict(self):
338 def test_fromdict(self):
339 c1 = Config({'Foo' : {'bar' : 1}})
339 c1 = Config({'Foo' : {'bar' : 1}})
340 self.assertEqual(c1.Foo.__class__, Config)
340 self.assertEqual(c1.Foo.__class__, Config)
341 self.assertEqual(c1.Foo.bar, 1)
341 self.assertEqual(c1.Foo.bar, 1)
342
342
343 def test_fromdictmerge(self):
343 def test_fromdictmerge(self):
344 c1 = Config()
344 c1 = Config()
345 c2 = Config({'Foo' : {'bar' : 1}})
345 c2 = Config({'Foo' : {'bar' : 1}})
346 c1.merge(c2)
346 c1.merge(c2)
347 self.assertEqual(c1.Foo.__class__, Config)
347 self.assertEqual(c1.Foo.__class__, Config)
348 self.assertEqual(c1.Foo.bar, 1)
348 self.assertEqual(c1.Foo.bar, 1)
349
349
350 def test_fromdictmerge2(self):
350 def test_fromdictmerge2(self):
351 c1 = Config({'Foo' : {'baz' : 2}})
351 c1 = Config({'Foo' : {'baz' : 2}})
352 c2 = Config({'Foo' : {'bar' : 1}})
352 c2 = Config({'Foo' : {'bar' : 1}})
353 c1.merge(c2)
353 c1.merge(c2)
354 self.assertEqual(c1.Foo.__class__, Config)
354 self.assertEqual(c1.Foo.__class__, Config)
355 self.assertEqual(c1.Foo.bar, 1)
355 self.assertEqual(c1.Foo.bar, 1)
356 self.assertEqual(c1.Foo.baz, 2)
356 self.assertEqual(c1.Foo.baz, 2)
357 self.assertNotIn('baz', c2.Foo)
357 self.assertNotIn('baz', c2.Foo)
358
358
359 def test_contains(self):
359 def test_contains(self):
360 c1 = Config({'Foo' : {'baz' : 2}})
360 c1 = Config({'Foo' : {'baz' : 2}})
361 c2 = Config({'Foo' : {'bar' : 1}})
361 c2 = Config({'Foo' : {'bar' : 1}})
362 self.assertIn('Foo', c1)
362 self.assertIn('Foo', c1)
363 self.assertIn('Foo.baz', c1)
363 self.assertIn('Foo.baz', c1)
364 self.assertIn('Foo.bar', c2)
364 self.assertIn('Foo.bar', c2)
365 self.assertNotIn('Foo.bar', c1)
365 self.assertNotIn('Foo.bar', c1)
366
366
367 def test_pickle_config(self):
367 def test_pickle_config(self):
368 cfg = Config()
368 cfg = Config()
369 cfg.Foo.bar = 1
369 cfg.Foo.bar = 1
370 pcfg = pickle.dumps(cfg)
370 pcfg = pickle.dumps(cfg)
371 cfg2 = pickle.loads(pcfg)
371 cfg2 = pickle.loads(pcfg)
372 self.assertEqual(cfg2, cfg)
372 self.assertEqual(cfg2, cfg)
373
373
374 def test_getattr_section(self):
374 def test_getattr_section(self):
375 cfg = Config()
375 cfg = Config()
376 self.assertNotIn('Foo', cfg)
376 self.assertNotIn('Foo', cfg)
377 Foo = cfg.Foo
377 Foo = cfg.Foo
378 assert isinstance(Foo, Config)
378 assert isinstance(Foo, Config)
379 self.assertIn('Foo', cfg)
379 self.assertIn('Foo', cfg)
380
380
381 def test_getitem_section(self):
381 def test_getitem_section(self):
382 cfg = Config()
382 cfg = Config()
383 self.assertNotIn('Foo', cfg)
383 self.assertNotIn('Foo', cfg)
384 Foo = cfg['Foo']
384 Foo = cfg['Foo']
385 assert isinstance(Foo, Config)
385 assert isinstance(Foo, Config)
386 self.assertIn('Foo', cfg)
386 self.assertIn('Foo', cfg)
387
387
388 def test_getattr_not_section(self):
388 def test_getattr_not_section(self):
389 cfg = Config()
389 cfg = Config()
390 self.assertNotIn('foo', cfg)
390 self.assertNotIn('foo', cfg)
391 foo = cfg.foo
391 foo = cfg.foo
392 assert isinstance(foo, LazyConfigValue)
392 assert isinstance(foo, LazyConfigValue)
393 self.assertIn('foo', cfg)
393 self.assertIn('foo', cfg)
394
394
395 def test_getattr_private_missing(self):
395 def test_getattr_private_missing(self):
396 cfg = Config()
396 cfg = Config()
397 self.assertNotIn('_repr_html_', cfg)
397 self.assertNotIn('_repr_html_', cfg)
398 with self.assertRaises(AttributeError):
398 with self.assertRaises(AttributeError):
399 _ = cfg._repr_html_
399 _ = cfg._repr_html_
400 self.assertNotIn('_repr_html_', cfg)
400 self.assertNotIn('_repr_html_', cfg)
401 self.assertEqual(len(cfg), 0)
401 self.assertEqual(len(cfg), 0)
402
402
403 def test_getitem_not_section(self):
403 def test_getitem_not_section(self):
404 cfg = Config()
404 cfg = Config()
405 self.assertNotIn('foo', cfg)
405 self.assertNotIn('foo', cfg)
406 foo = cfg['foo']
406 foo = cfg['foo']
407 assert isinstance(foo, LazyConfigValue)
407 assert isinstance(foo, LazyConfigValue)
408 self.assertIn('foo', cfg)
408 self.assertIn('foo', cfg)
409
409
410 def test_merge_copies(self):
410 def test_merge_copies(self):
411 c = Config()
411 c = Config()
412 c2 = Config()
412 c2 = Config()
413 c2.Foo.trait = []
413 c2.Foo.trait = []
414 c.merge(c2)
414 c.merge(c2)
415 c2.Foo.trait.append(1)
415 c2.Foo.trait.append(1)
416 self.assertIsNot(c.Foo, c2.Foo)
416 self.assertIsNot(c.Foo, c2.Foo)
417 self.assertEqual(c.Foo.trait, [])
417 self.assertEqual(c.Foo.trait, [])
418 self.assertEqual(c2.Foo.trait, [1])
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
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