##// END OF EJS Templates
More work on traitlets.py. I have added tests for existing traitlets.
More work on traitlets.py. I have added tests for existing traitlets.

File last commit:

r2175:2e2824fd
r2175:2e2824fd
Show More
traitlets.py
436 lines | 12.9 KiB | text/x-python | PythonLexer
#!/usr/bin/env python
# encoding: utf-8
"""
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
* 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 inspect
import types
from types import InstanceType
#-----------------------------------------------------------------------------
# Basic classes
#-----------------------------------------------------------------------------
class NoDefaultSpecified ( object ): pass
NoDefaultSpecified = NoDefaultSpecified()
class Undefined ( object ): pass
Undefined = Undefined()
class TraitletError(Exception):
pass
#-----------------------------------------------------------------------------
# Utilities
#-----------------------------------------------------------------------------
def class_of ( object ):
""" Returns a string containing the class name of an object with the
correct indefinite article ('a' or 'an') preceding it (e.g., 'an Image',
'a PlotValue').
"""
if isinstance( object, basestring ):
return add_article( object )
return add_article( object.__class__.__name__ )
def add_article ( name ):
""" Returns a string containing the correct indefinite article ('a' or 'an')
prefixed to the specified string.
"""
if name[:1].lower() in 'aeiou':
return 'an ' + name
return 'a ' + name
def repr_type(obj):
""" Return a string representation of a value and its type for readable
error messages.
"""
the_type = type(obj)
if the_type is InstanceType:
# Old-style class.
the_type = obj.__class__
msg = '%r %r' % (obj, the_type)
return msg
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:
return ['anytraitlet']
elif isinstance(name, (list, tuple)):
for n in name:
assert isinstance(n, str), "names must be strings"
return name
#-----------------------------------------------------------------------------
# Base TraitletType for all traitlets
#-----------------------------------------------------------------------------
class TraitletType(object):
metadata = {}
default_value = None
info_text = 'any value'
def __init__(self, default_value=NoDefaultSpecified, **metadata):
if default_value is not NoDefaultSpecified:
self.default_value = default_value
self.metadata.update(metadata)
def __get__(self, inst, cls=None):
if inst is None:
return self
else:
return inst._traitlet_values.get(self.name, self.default_value)
def __set__(self, inst, value):
new_value = self._validate(inst, value)
old_value = self.__get__(inst)
if old_value != new_value:
inst._traitlet_values[self.name] = new_value
inst._notify(self.name, old_value, new_value)
def _validate(self, inst, value):
if hasattr(self, 'validate'):
return self.validate(inst, value)
elif hasattr(self, 'is_valid_for'):
valid = self.is_valid_for(value)
if valid:
return value
else:
raise TraitletError('invalid value for type: %r' % value)
elif hasattr(self, 'value_for'):
return self.value_for(value)
else:
return value
def info(self):
return self.info_text
def error(self, obj, value):
if obj is not None:
e = "The '%s' traitlet of %s instance must be %s, but a value of %s was specified." \
% (self.name, class_of(obj),
self.info(), repr_type(value))
else:
e = "The '%s' traitlet must be %s, but a value of %r was specified." \
% (self.name, self.info(), repr_type(value))
raise TraitletError(e)
#-----------------------------------------------------------------------------
# The HasTraitlets implementation
#-----------------------------------------------------------------------------
class MetaHasTraitlets(type):
"""A metaclass for HasTraitlets.
This metaclass makes sure that any TraitletType class attributes are
instantiated and sets their name attribute.
"""
def __new__(mcls, name, bases, classdict):
for k,v in classdict.iteritems():
if isinstance(v, TraitletType):
v.name = k
elif inspect.isclass(v):
if issubclass(v, TraitletType):
vinst = v()
vinst.name = k
classdict[k] = vinst
return super(MetaHasTraitlets, mcls).__new__(mcls, name, bases, classdict)
class HasTraitlets(object):
__metaclass__ = MetaHasTraitlets
def __init__(self):
self._traitlet_values = {}
self._traitlet_notifiers = {}
def _notify(self, name, old_value, new_value):
# 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
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._traitlet_notifiers.has_key(name):
nlist = []
self._traitlet_notifiers[name] = nlist
else:
nlist = self._traitlet_notifiers[name]
if handler not in nlist:
nlist.append(handler)
def _remove_notifiers(self, handler, name):
if self._traitlet_notifiers.has_key(name):
nlist = self._traitlet_notifiers[name]
try:
index = nlist.index(handler)
except ValueError:
pass
else:
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:
self._remove_notifiers(handler, n)
else:
names = parse_notifier_name(name)
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
#-----------------------------------------------------------------------------
class Any(TraitletType):
default_value = None
info_text = 'any value'
class Int(TraitletType):
evaluate = int
default_value = 0
info_text = 'an integer'
def validate(self, obj, value):
if isinstance(value, int):
return value
self.error(obj, value)
class Long(TraitletType):
evaluate = long
default_value = 0L
info_text = 'a long'
def validate(self, obj, value):
if isinstance(value, long):
return value
if isinstance(value, int):
return long(value)
self.error(obj, value)
class Float(TraitletType):
evaluate = float
default_value = 0.0
info_text = 'a float'
def validate(self, obj, value):
if isinstance(value, float):
return value
if isinstance(value, int):
return float(value)
self.error(obj, value)
class Complex(TraitletType):
evaluate = complex
default_value = 0.0 + 0.0j
info_text = 'a complex number'
def validate(self, obj, value):
if isinstance(value, complex):
return value
if isinstance(value, (float, int)):
return complex(value)
self.error(obj, value)
class Str(TraitletType):
evaluate = lambda x: x
default_value = ''
info_text = 'a string'
def validate(self, obj, value):
if isinstance(value, str):
return value
self.error(obj, value)
class Unicode(TraitletType):
evaluate = unicode
default_value = u''
info_text = 'a unicode string'
def validate(self, obj, value):
if isinstance(value, unicode):
return value
if isinstance(value, str):
return unicode(value)
self.error(obj, value)
class Bool(TraitletType):
evaluate = bool
default_value = False
info_text = 'a boolean'
def validate(self, obj, value):
if isinstance(value, bool):
return value
self.error(obj, value)