#!/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)