diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py new file mode 100644 index 0000000..294f376 --- /dev/null +++ b/IPython/utils/tests/test_traitlets.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +Tests for IPython.utils.traitlets. + +Authors: + +* Brian Granger +* Enthought, Inc. Some of the code in this file comes from enthought.traits + and is licensed under the BSD license. Also, many of the ideas also come + from enthought.traits even though our implementation is very different. +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2008-2009 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import sys +import os + + +from unittest import TestCase + +from IPython.utils.traitlets import ( + HasTraitlets, MetaHasTraitlets, TraitletType, Any, + Int, Long, Float, Complex, Str, Unicode, Bool, TraitletError +) + + +#----------------------------------------------------------------------------- +# Helper classes for testing +#----------------------------------------------------------------------------- + + +class HasTraitletsStub(object): + + def __init__(self): + self._traitlet_values = {} + self._traitlet_notifiers = {} + + def _notify(self, name, old, new): + self._notify_name = name + self._notify_old = old + self._notify_new = new + + +#----------------------------------------------------------------------------- +# Test classes +#----------------------------------------------------------------------------- + + +class TestTraitletType(TestCase): + + name = 'a' + + def setUp(self): + self.tt = TraitletType() + self.tt.name = self.name + self.hast = HasTraitletsStub() + + def test_get(self): + value = self.tt.__get__(self.hast) + self.assertEquals(value, None) + + def test_set(self): + self.tt.__set__(self.hast, 10) + self.assertEquals(self.hast._traitlet_values[self.name],10) + self.assertEquals(self.hast._notify_name,self.name) + self.assertEquals(self.hast._notify_old,None) + self.assertEquals(self.hast._notify_new,10) + + def test_validate(self): + class MyTT(TraitletType): + def validate(self, inst, value): + return -1 + tt = MyTT() + tt.name = self.name + tt.__set__(self.hast, 10) + self.assertEquals(tt.__get__(self.hast),-1) + + def test_is_valid_for(self): + class MyTT(TraitletType): + def is_valid_for(self, value): + return True + tt = MyTT() + tt.name = self.name + tt.__set__(self.hast, 10) + self.assertEquals(tt.__get__(self.hast),10) + + def test_value_for(self): + class MyTT(TraitletType): + def value_for(self, value): + return 20 + tt = MyTT() + tt.name = self.name + tt.__set__(self.hast, 10) + self.assertEquals(tt.__get__(self.hast),20) + + def test_info(self): + self.assertEquals(self.tt.info(),'any value') + + def test_error(self): + self.assertRaises(TraitletError, self.tt.error, self.hast, 10) + + +class TestHasTraitletsMeta(TestCase): + + def test_metaclass(self): + self.assertEquals(type(HasTraitlets), MetaHasTraitlets) + + class A(HasTraitlets): + a = Int + + a = A() + self.assertEquals(type(a.__class__), MetaHasTraitlets) + self.assertEquals(a.a,0) + a.a = 10 + self.assertEquals(a.a,10) + + class B(HasTraitlets): + b = Int() + + b = B() + self.assertEquals(b.b,0) + b.b = 10 + self.assertEquals(b.b,10) + + class C(HasTraitlets): + c = Int(30) + + c = C() + self.assertEquals(c.c,30) + c.c = 10 + self.assertEquals(c.c,10) + + +class TestHasTraitletsNotify(TestCase): + + def setUp(self): + self._notify1 = [] + self._notify2 = [] + + def notify1(self, name, old, new): + self._notify1.append((name, old, new)) + + def notify2(self, name, old, new): + self._notify2.append((name, old, new)) + + def test_notify_all(self): + + class A(HasTraitlets): + a = Int + b = Float + + a = A() + a.on_traitlet_change(self.notify1) + a.a = 0 + self.assertEquals(len(self._notify1),0) + a.b = 0.0 + self.assertEquals(len(self._notify1),0) + a.a = 10 + self.assert_(('a',0,10) in self._notify1) + a.b = 10.0 + self.assert_(('b',0.0,10.0) in self._notify1) + self.assertRaises(TraitletError,setattr,a,'a','bad string') + self.assertRaises(TraitletError,setattr,a,'b','bad string') + self._notify1 = [] + a.on_traitlet_change(self.notify1,remove=True) + a.a = 20 + a.b = 20.0 + self.assertEquals(len(self._notify1),0) + + def test_notify_one(self): + + class A(HasTraitlets): + a = Int + b = Float + + a = A() + a.on_traitlet_change(self.notify1, 'a') + a.a = 0 + self.assertEquals(len(self._notify1),0) + a.a = 10 + self.assert_(('a',0,10) in self._notify1) + self.assertRaises(TraitletError,setattr,a,'a','bad string') + + def test_subclass(self): + + class A(HasTraitlets): + a = Int + + class B(A): + b = Float + + b = B() + self.assertEquals(b.a,0) + self.assertEquals(b.b,0.0) + b.a = 100 + b.b = 100.0 + self.assertEquals(b.a,100) + self.assertEquals(b.b,100.0) + + def test_notify_subclass(self): + + class A(HasTraitlets): + a = Int + + class B(A): + b = Float + + b = B() + b.on_traitlet_change(self.notify1, 'a') + b.on_traitlet_change(self.notify2, 'b') + b.a = 0 + b.b = 0.0 + self.assertEquals(len(self._notify1),0) + self.assertEquals(len(self._notify2),0) + b.a = 10 + b.b = 10.0 + self.assert_(('a',0,10) in self._notify1) + self.assert_(('b',0.0,10.0) in self._notify2) + + def test_static_notify(self): + + class A(HasTraitlets): + a = Int + _notify1 = [] + def _a_changed(self, name, old, new): + self._notify1.append((name, old, new)) + + a = A() + a.a = 0 + self.assertEquals(len(a._notify1),0) + a.a = 10 + self.assert_(('a',0,10) in a._notify1) + + class B(A): + b = Float + _notify2 = [] + def _b_changed(self, name, old, new): + self._notify2.append((name, old, new)) + + b = B() + b.a = 10 + b.b = 10.0 + self.assert_(('a',0,10) in b._notify1) + self.assert_(('b',0.0,10.0) in b._notify2) + + def test_notify_args(self): + + def callback0(): + self.cb = () + def callback1(name): + self.cb = (name,) + def callback2(name, new): + self.cb = (name, new) + def callback3(name, old, new): + self.cb = (name, old, new) + + class A(HasTraitlets): + a = Int + + a = A() + a.on_traitlet_change(callback0, 'a') + a.a = 10 + self.assertEquals(self.cb,()) + a.on_traitlet_change(callback0, 'a', remove=True) + + a.on_traitlet_change(callback1, 'a') + a.a = 100 + self.assertEquals(self.cb,('a',)) + a.on_traitlet_change(callback1, 'a', remove=True) + + a.on_traitlet_change(callback2, 'a') + a.a = 1000 + self.assertEquals(self.cb,('a',1000)) + a.on_traitlet_change(callback2, 'a', remove=True) + + a.on_traitlet_change(callback3, 'a') + a.a = 10000 + self.assertEquals(self.cb,('a',1000,10000)) + a.on_traitlet_change(callback3, 'a', remove=True) + + self.assertEquals(len(a._traitlet_notifiers['a']),0) + + +class TestAddTraitlet(TestCase): + + def test_add_float(self): + + class A(HasTraitlets): + a = Int + + a = A() + a.a = 10 + a._add_class_traitlet('b',Float) + self.assertEquals(a.b,0.0) + a.b = 10.0 + self.assertEquals(a.b,10.0) + self.assertRaises(TraitletError, setattr, a, 'b', 'bad value') + + +#----------------------------------------------------------------------------- +# Tests for specific traitlet types +#----------------------------------------------------------------------------- + +class TraitletTestBase(TestCase): + + def assign(self, value): + self.obj.value = value + + def coerce(self, value): + return value + + def test_good_values(self): + if hasattr(self, '_good_values'): + for value in self._good_values: + self.assign(value) + self.assertEquals(self.obj.value, self.coerce(value)) + + def test_bad_values(self): + if hasattr(self, '_bad_values'): + for value in self._bad_values: + self.assertRaises(TraitletError, self.assign, value) + + def test_default_value(self): + if hasattr(self, '_default_value'): + self.assertEquals(self._default_value, self.obj.value) + + +class AnyTraitlet(HasTraitlets): + + value = Any + +class AnyTraitTest(TraitletTestBase): + + obj = AnyTraitlet() + + _default_value = None + _good_values = [10.0, 'ten', u'ten', [10], {'ten': 10},(10,), None, 1j] + _bad_values = [] + + +class IntTraitlet(HasTraitlets): + + value = Int(99) + +class TestInt(TraitletTestBase): + + obj = IntTraitlet() + _default_value = 99 + _good_values = [10, -10] + _bad_values = ['ten', u'ten', [10], {'ten': 10},(10,), None, 1j, 10L, + -10L, 10.1, -10.1, '10L', '-10L', '10.1', '-10.1', u'10L', + u'-10L', u'10.1', u'-10.1', '10', '-10', u'10', u'-10'] + + +class LongTraitlet(HasTraitlets): + + value = Long(99L) + +class TestLong(TraitletTestBase): + + obj = LongTraitlet() + + _default_value = 99L + _good_values = [10, -10, 10L, -10L] + _bad_values = ['ten', u'ten', [10], [10l], {'ten': 10},(10,),(10L,), + None, 1j, 10.1, -10.1, '10', '-10', '10L', '-10L', '10.1', + '-10.1', u'10', u'-10', u'10L', u'-10L', u'10.1', + u'-10.1'] + + +class FloatTraitlet(HasTraitlets): + + value = Float(99.0) + +class TestFloat(TraitletTestBase): + + obj = FloatTraitlet() + + _default_value = 99.0 + _good_values = [10, -10, 10.1, -10.1] + _bad_values = [10L, -10L, 'ten', u'ten', [10], {'ten': 10},(10,), None, + 1j, '10', '-10', '10L', '-10L', '10.1', '-10.1', u'10', + u'-10', u'10L', u'-10L', u'10.1', u'-10.1'] + + +class ComplexTraitlet(HasTraitlets): + + value = Complex(99.0-99.0j) + +class TestComplex(TraitletTestBase): + + obj = ComplexTraitlet() + + _default_value = 99.0-99.0j + _good_values = [10, -10, 10.1, -10.1, 10j, 10+10j, 10-10j, + 10.1j, 10.1+10.1j, 10.1-10.1j] + _bad_values = [10L, -10L, u'10L', u'-10L', 'ten', [10], {'ten': 10},(10,), None] + + +class StringTraitlet(HasTraitlets): + + value = Str('string') + +class TestString(TraitletTestBase): + + obj = StringTraitlet() + + _default_value = 'string' + _good_values = ['10', '-10', '10L', + '-10L', '10.1', '-10.1', 'string'] + _bad_values = [10, -10, 10L, -10L, 10.1, -10.1, 1j, [10], + ['ten'],{'ten': 10},(10,), None, u'string'] + + +class UnicodeTraitlet(HasTraitlets): + + value = Unicode(u'unicode') + +class TestUnicode(TraitletTestBase): + + obj = UnicodeTraitlet() + + _default_value = u'unicode' + _good_values = ['10', '-10', '10L', '-10L', '10.1', + '-10.1', '', u'', 'string', u'string', ] + _bad_values = [10, -10, 10L, -10L, 10.1, -10.1, 1j, + [10], ['ten'], [u'ten'], {'ten': 10},(10,), None] diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index ad2f343..ea574d0 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -3,6 +3,26 @@ """ A lightweight Traits like module. +This is designed to provide a lightweight, simple, pure Python version of +many of the capabilities of enthought.traits. This includes: + +* Validation +* Type specification with defaults +* Static and dynamic notification +* Basic predefined types +* An API that is similar to enthought.traits + +We don't support: + +* Delegation +* Automatic GUI generation +* A full set of trait types +* API compatibility with enthought.traits + +We choose to create this module because we need these capabilities, but +we need them to be pure Python so they work in all Python implementations, +including Jython and IronPython. + Authors: * Brian Granger @@ -82,6 +102,18 @@ def repr_type(obj): def parse_notifier_name(name): + """Convert the name argument to a list of names. + + Examples + -------- + + >>> parse_notifier_name('a') + ['a'] + >>> parse_notifier_name(['a','b']) + ['a', 'b'] + >>> parse_notifier_name(None) + ['anytraitlet'] + """ if isinstance(name, str): return [name] elif name is None: @@ -103,8 +135,6 @@ class TraitletType(object): default_value = None info_text = 'any value' - # def __init__(self, name, default_value=NoDefaultSpecified, **metadata): - # self.name = name def __init__(self, default_value=NoDefaultSpecified, **metadata): if default_value is not NoDefaultSpecified: self.default_value = default_value @@ -121,7 +151,7 @@ class TraitletType(object): old_value = self.__get__(inst) if old_value != new_value: inst._traitlet_values[self.name] = new_value - inst._notify(self.name, old_value, value) + inst._notify(self.name, old_value, new_value) def _validate(self, inst, value): if hasattr(self, 'validate'): @@ -181,28 +211,64 @@ class HasTraitlets(object): def __init__(self): self._traitlet_values = {} - self._notifiers = {} + self._traitlet_notifiers = {} def _notify(self, name, old_value, new_value): - callables = self._notifiers.get(name,[]) - more_callables = self._notifiers.get('anytraitlet',[]) + + # First dynamic ones + callables = self._traitlet_notifiers.get(name,[]) + more_callables = self._traitlet_notifiers.get('anytraitlet',[]) callables.extend(more_callables) + + # Now static ones + try: + cb = getattr(self, '_%s_changed' % name) + except: + pass + else: + callables.append(cb) + + # Call them all now for c in callables: # Traits catches and logs errors here. I allow them to raise - c(name, old_value, new_value) + if callable(c): + argspec = inspect.getargspec(c) + nargs = len(argspec[0]) + # Bound methods have an additional 'self' argument + # I don't know how to treat unbound methods, but they + # can't really be used for callbacks. + if isinstance(c, types.MethodType): + offset = -1 + else: + offset = 0 + if nargs + offset == 0: + c() + elif nargs + offset == 1: + c(name) + elif nargs + offset == 2: + c(name, new_value) + elif nargs + offset == 3: + c(name, old_value, new_value) + else: + raise TraitletError('a traitlet changed callback ' + 'must have 0-3 arguments.') + else: + raise TraitletError('a traitlet changed callback ' + 'must be callable.') + def _add_notifiers(self, handler, name): - if not self._notifiers.has_key(name): + if not self._traitlet_notifiers.has_key(name): nlist = [] - self._notifiers[name] = nlist + self._traitlet_notifiers[name] = nlist else: - nlist = self._notifiers[name] + nlist = self._traitlet_notifiers[name] if handler not in nlist: nlist.append(handler) def _remove_notifiers(self, handler, name): - if self._notifiers.has_key(name): - nlist = self._notifiers[name] + if self._traitlet_notifiers.has_key(name): + nlist = self._traitlet_notifiers[name] try: index = nlist.index(handler) except ValueError: @@ -211,6 +277,30 @@ class HasTraitlets(object): del nlist[index] def on_traitlet_change(self, handler, name=None, remove=False): + """Setup a handler to be called when a traitlet changes. + + This is used to setup dynamic notifications of traitlet changes. + + Static handlers can be created by creating methods on a HasTraitlets + subclass with the naming convention '_[traitletname]_changed'. Thus, + to create static handler for the traitlet 'a', create the method + _a_changed(self, name, old, new) (fewer arguments can be used, see + below). + + Parameters + ---------- + handler : callable + A callable that is called when a traitlet changes. Its + signature can be handler(), handler(name), handler(name, new) + or handler(name, old, new). + name : list, str, None + If None, the handler will apply to all traitlets. If a list + of str, handler will apply to all names in the list. If a + str, the handler will apply just to that name. + remove : bool + If False (the default), then install the handler. If True + then unintall it. + """ if remove: names = parse_notifier_name(name) for n in names: @@ -220,6 +310,28 @@ class HasTraitlets(object): for n in names: self._add_notifiers(handler, n) + def _add_class_traitlet(self, name, traitlet): + """Add a class-level traitlet. + + This create a new traitlet attached to all instances of this class. + But, the value can be different on each instance. But, this behavior + is likely to trip up many folks as they would expect the traitlet + type to be different on each instance. + + Parameters + ---------- + name : str + The name of the traitlet. + traitlet : TraitletType or an instance of one + The traitlet to assign to the name. + """ + if inspect.isclass(traitlet): + inst = traitlet() + else: + inst = traitlet + assert isinstance(inst, TraitletType) + inst.name = name + setattr(self.__class__, name, inst) #----------------------------------------------------------------------------- # Actual TraitletTypes implementations/subclasses