traitlets.py
436 lines
| 12.9 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2157 | #!/usr/bin/env python | ||
# encoding: utf-8 | ||||
""" | ||||
A lightweight Traits like module. | ||||
Brian Granger
|
r2175 | 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. | ||||
Brian Granger
|
r2157 | 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): | ||||
Brian Granger
|
r2175 | """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'] | ||||
""" | ||||
Brian Granger
|
r2157 | 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 | ||||
Brian Granger
|
r2175 | inst._notify(self.name, old_value, new_value) | ||
Brian Granger
|
r2157 | |||
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 = {} | ||||
Brian Granger
|
r2175 | self._traitlet_notifiers = {} | ||
Brian Granger
|
r2157 | |||
def _notify(self, name, old_value, new_value): | ||||
Brian Granger
|
r2175 | |||
# First dynamic ones | ||||
callables = self._traitlet_notifiers.get(name,[]) | ||||
more_callables = self._traitlet_notifiers.get('anytraitlet',[]) | ||||
Brian Granger
|
r2157 | callables.extend(more_callables) | ||
Brian Granger
|
r2175 | |||
# Now static ones | ||||
try: | ||||
cb = getattr(self, '_%s_changed' % name) | ||||
except: | ||||
pass | ||||
else: | ||||
callables.append(cb) | ||||
# Call them all now | ||||
Brian Granger
|
r2157 | for c in callables: | ||
# Traits catches and logs errors here. I allow them to raise | ||||
Brian Granger
|
r2175 | 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.') | ||||
Brian Granger
|
r2157 | |||
def _add_notifiers(self, handler, name): | ||||
Brian Granger
|
r2175 | if not self._traitlet_notifiers.has_key(name): | ||
Brian Granger
|
r2157 | nlist = [] | ||
Brian Granger
|
r2175 | self._traitlet_notifiers[name] = nlist | ||
Brian Granger
|
r2157 | else: | ||
Brian Granger
|
r2175 | nlist = self._traitlet_notifiers[name] | ||
Brian Granger
|
r2157 | if handler not in nlist: | ||
nlist.append(handler) | ||||
def _remove_notifiers(self, handler, name): | ||||
Brian Granger
|
r2175 | if self._traitlet_notifiers.has_key(name): | ||
nlist = self._traitlet_notifiers[name] | ||||
Brian Granger
|
r2157 | try: | ||
index = nlist.index(handler) | ||||
except ValueError: | ||||
pass | ||||
else: | ||||
del nlist[index] | ||||
def on_traitlet_change(self, handler, name=None, remove=False): | ||||
Brian Granger
|
r2175 | """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. | ||||
""" | ||||
Brian Granger
|
r2157 | 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) | ||||
Brian Granger
|
r2175 | 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) | ||||
Brian Granger
|
r2157 | |||
#----------------------------------------------------------------------------- | ||||
# 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) | ||||