diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py index ca30c84..8dd2cd9 100755 --- a/IPython/utils/tests/test_traitlets.py +++ b/IPython/utils/tests/test_traitlets.py @@ -25,9 +25,9 @@ Authors: from unittest import TestCase from IPython.utils.traitlets import ( - HasTraits, MetaHasTraits, TraitType, Any, + HasTraits, MetaHasTraits, TraitType, Any, CStr, Int, Long, Float, Complex, Str, Unicode, TraitError, - Undefined, Type, This, Instance, TCPAddress + Undefined, Type, This, Instance, TCPAddress, List, Tuple ) @@ -741,3 +741,74 @@ class TestTCPAddress(TraitTestBase): _default_value = ('127.0.0.1',0) _good_values = [('localhost',0),('192.168.0.1',1000),('www.google.com',80)] _bad_values = [(0,0),('localhost',10.0),('localhost',-1)] + +class ListTrait(HasTraits): + + value = List(Int) + +class TestList(TraitTestBase): + + obj = ListTrait() + + _default_value = [] + _good_values = [[], [1], range(10)] + _bad_values = [10, [1,'a'], 'a', (1,2)] + +class LenListTrait(HasTraits): + + value = List(Int, [0], minlen=1, maxlen=2) + +class TestLenList(TraitTestBase): + + obj = LenListTrait() + + _default_value = [0] + _good_values = [[1], range(2)] + _bad_values = [10, [1,'a'], 'a', (1,2), [], range(3)] + +class TupleTrait(HasTraits): + + value = Tuple(Int) + +class TestTupleTrait(TraitTestBase): + + obj = TupleTrait() + + _default_value = None + _good_values = [(1,), None,(0,)] + _bad_values = [10, (1,2), [1],('a'), ()] + + def test_invalid_args(self): + self.assertRaises(TypeError, Tuple, 5) + self.assertRaises(TypeError, Tuple, default_value='hello') + t = Tuple(Int, CStr, default_value=(1,5)) + +class LooseTupleTrait(HasTraits): + + value = Tuple((1,2,3)) + +class TestLooseTupleTrait(TraitTestBase): + + obj = LooseTupleTrait() + + _default_value = (1,2,3) + _good_values = [(1,), None, (0,), tuple(range(5)), tuple('hello'), ('a',5), ()] + _bad_values = [10, 'hello', [1], []] + + def test_invalid_args(self): + self.assertRaises(TypeError, Tuple, 5) + self.assertRaises(TypeError, Tuple, default_value='hello') + t = Tuple(Int, CStr, default_value=(1,5)) + + +class MultiTupleTrait(HasTraits): + + value = Tuple(Int, Str, default_value=[99,'bottles']) + +class TestMultiTuple(TraitTestBase): + + obj = MultiTupleTrait() + + _default_value = (99,'bottles') + _good_values = [(1,'a'), (2,'b')] + _bad_values = ((),10, 'a', (1,'a',3), ('a',1)) diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index 21187cc..c4c8b12 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -328,7 +328,7 @@ class TraitType(object): self.info(), repr_type(value)) else: e = "The '%s' trait must be %s, but a value of %r was specified." \ - % (self.name, self.info(), repr_type(value)) + % (self.name, self.info(), repr_type(value)) raise TraitError(e) def get_metadata(self, key): @@ -621,7 +621,15 @@ class ClassBasedTraitType(TraitType): else: msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) ) - super(ClassBasedTraitType, self).error(obj, msg) + if obj is not None: + e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \ + % (self.name, class_of(obj), + self.info(), msg) + else: + e = "The '%s' trait must be %s, but a value of %r was specified." \ + % (self.name, self.info(), msg) + + raise TraitError(e) class Type(ClassBasedTraitType): @@ -1055,46 +1063,255 @@ class CaselessStrEnum(Enum): return v self.error(obj, value) +class Container(Instance): + """An instance of a container (list, set, etc.) -class List(Instance): - """An instance of a Python list.""" + To be subclassed by overriding klass. + """ + klass = None + _valid_defaults = SequenceTypes + _trait = None - def __init__(self, default_value=None, allow_none=True, **metadata): - """Create a list trait type from a list, set, or tuple. + def __init__(self, trait=None, default_value=None, allow_none=True, + **metadata): + """Create a container trait type from a list, set, or tuple. - The default value is created by doing ``list(default_value)``, + The default value is created by doing ``List(default_value)``, which creates a copy of the ``default_value``. + + ``trait`` can be specified, which restricts the type of elements + in the container to that TraitType. + + If only one arg is given and it is not a Trait, it is taken as + ``default_value``: + + ``c = List([1,2,3])`` + + Parameters + ---------- + + trait : TraitType [ optional ] + the type for restricting the contents of the Container. If unspecified, + types are not checked. + + default_value : SequenceType [ optional ] + The default value for the Trait. Must be list/tuple/set, and + will be cast to the container type. + + allow_none : Bool [ default True ] + Whether to allow the value to be None + + **metadata : any + further keys for extensions to the Trait (e.g. config) + """ + istrait = lambda t: isinstance(t, type) and issubclass(t, TraitType) + + # allow List([values]): + if default_value is None and not istrait(trait): + default_value = trait + trait = None + if default_value is None: - args = ((),) - elif isinstance(default_value, SequenceTypes): + args = () + elif isinstance(default_value, self._valid_defaults): args = (default_value,) else: - raise TypeError('default value of List was %s' % default_value) + raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value)) + + if istrait(trait): + self._trait = trait() + self._trait.name = 'element' + elif trait is not None: + raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait)) - super(List,self).__init__(klass=list, args=args, + super(Container,self).__init__(klass=self.klass, args=args, allow_none=allow_none, **metadata) + def element_error(self, obj, element, validator): + e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \ + % (self.name, class_of(obj), validator.info(), repr_type(element)) + raise TraitError(e) -class Set(Instance): - """An instance of a Python set.""" + def validate(self, obj, value): + value = super(Container, self).validate(obj, value) + if value is None: + return value - def __init__(self, default_value=None, allow_none=True, **metadata): - """Create a set trait type from a set, list, or tuple. + value = self.validate_elements(obj, value) + + return value + + def validate_elements(self, obj, value): + validated = [] + if self._trait is None or isinstance(self._trait, Any): + return value + for v in value: + try: + v = self._trait.validate(obj, v) + except TraitError: + self.element_error(obj, v, self._trait) + else: + validated.append(v) + return self.klass(validated) + + +class List(Container): + """An instance of a Python list.""" + klass = list - The default value is created by doing ``set(default_value)``, + def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxint, + allow_none=True, **metadata): + """Create a List trait type from a list, set, or tuple. + + The default value is created by doing ``List(default_value)``, which creates a copy of the ``default_value``. + + ``trait`` can be specified, which restricts the type of elements + in the container to that TraitType. + + If only one arg is given and it is not a Trait, it is taken as + ``default_value``: + + ``c = List([1,2,3])`` + + Parameters + ---------- + + trait : TraitType [ optional ] + the type for restricting the contents of the Container. If unspecified, + types are not checked. + + default_value : SequenceType [ optional ] + The default value for the Trait. Must be list/tuple/set, and + will be cast to the container type. + + minlen : Int [ default 0 ] + The minimum length of the input list + + maxlen : Int [ default sys.maxint ] + The maximum length of the input list + + allow_none : Bool [ default True ] + Whether to allow the value to be None + + **metadata : any + further keys for extensions to the Trait (e.g. config) + """ + self._minlen = minlen + self._maxlen = maxlen + super(List, self).__init__(trait=trait, default_value=default_value, + allow_none=allow_none, **metadata) + + def length_error(self, obj, value): + e = "The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified." \ + % (self.name, class_of(obj), self._minlen, self._maxlen, value) + raise TraitError(e) + + def validate_elements(self, obj, value): + length = len(value) + if length < self._minlen or length > self._maxlen: + self.length_error(obj, value) + + return super(List, self).validate_elements(obj, value) + + +class Set(Container): + """An instance of a Python set.""" + klass = set + +class Tuple(Container): + """An instance of a Python tuple.""" + klass = tuple + + def __init__(self, *traits, **metadata): + """Tuple(*traits, default_value=None, allow_none=True, **medatata) + + Create a tuple from a list, set, or tuple. + + Create a fixed-type tuple with Traits: + + ``t = Tuple(Int, Str, CStr)`` + + would be length 3, with Int,Str,CStr for each element. + + If only one arg is given and it is not a Trait, it is taken as + default_value: + + ``t = Tuple((1,2,3))`` + + Otherwise, ``default_value`` *must* be specified by keyword. + + Parameters + ---------- + + *traits : TraitTypes [ optional ] + the tsype for restricting the contents of the Tuple. If unspecified, + types are not checked. If specified, then each positional argument + corresponds to an element of the tuple. Tuples defined with traits + are of fixed length. + + default_value : SequenceType [ optional ] + The default value for the Tuple. Must be list/tuple/set, and + will be cast to a tuple. If `traits` are specified, the + `default_value` must conform to the shape and type they specify. + + allow_none : Bool [ default True ] + Whether to allow the value to be None + + **metadata : any + further keys for extensions to the Trait (e.g. config) + + """ + default_value = metadata.pop('default_value', None) + allow_none = metadata.pop('allow_none', True) + + istrait = lambda t: isinstance(t, type) and issubclass(t, TraitType) + + # allow Tuple((values,)): + if len(traits) == 1 and default_value is None and not istrait(traits[0]): + default_value = traits[0] + traits = () + if default_value is None: - args = ((),) - elif isinstance(default_value, SequenceTypes): + args = () + elif isinstance(default_value, self._valid_defaults): args = (default_value,) else: - raise TypeError('default value of Set was %s' % default_value) - - super(Set,self).__init__(klass=set, args=args, + raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value)) + + self._traits = [] + for trait in traits: + t = trait() + t.name = 'element' + self._traits.append(t) + + if self._traits and default_value is None: + # don't allow default to be an empty container if length is specified + args = None + super(Container,self).__init__(klass=self.klass, args=args, allow_none=allow_none, **metadata) + def validate_elements(self, obj, value): + if not self._traits: + # nothing to validate + return value + if len(value) != len(self._traits): + e = "The '%s' trait of %s instance requires %i elements, but a value of %s was specified." \ + % (self.name, class_of(obj), len(self._traits), repr_type(value)) + raise TraitError(e) + + validated = [] + for t,v in zip(self._traits, value): + try: + v = t.validate(obj, v) + except TraitError: + self.element_error(obj, v, t) + else: + validated.append(v) + return tuple(validated) + class Dict(Instance): """An instance of a Python dict.""" @@ -1117,7 +1334,6 @@ class Dict(Instance): super(Dict,self).__init__(klass=dict, args=args, allow_none=allow_none, **metadata) - class TCPAddress(TraitType): """A trait for an (ip, port) tuple.