traitlets.py
1075 lines
| 34.0 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 | ||||
Brian Granger
|
r2179 | * A full set of trait types. Most importantly, we don't provide container | ||
Dav Clark
|
r2385 | traits (list, dict, tuple) that can trigger notifications if their | ||
Brian Granger
|
r2179 | contents change. | ||
Brian Granger
|
r2175 | * API compatibility with enthought.traits | ||
Brian Granger
|
r2179 | There are also some important difference in our design: | ||
* enthought.traits does not validate default values. We do. | ||||
Brian Granger
|
r2175 | 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 | ||||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r2177 | |||
Brian Granger
|
r2157 | import inspect | ||
Brian Granger
|
r2177 | import sys | ||
Brian Granger
|
r2157 | import types | ||
Brian Granger
|
r2204 | from types import ( | ||
InstanceType, ClassType, FunctionType, | ||||
ListType, TupleType | ||||
) | ||||
Thomas Kluyver
|
r3108 | from .importstring import import_item | ||
Brian Granger
|
r2229 | |||
Brian Granger
|
r2177 | ClassTypes = (ClassType, type) | ||
Brian Granger
|
r2157 | |||
Brian Granger
|
r2204 | SequenceTypes = (ListType, TupleType) | ||
Brian Granger
|
r2157 | #----------------------------------------------------------------------------- | ||
# Basic classes | ||||
#----------------------------------------------------------------------------- | ||||
class NoDefaultSpecified ( object ): pass | ||||
NoDefaultSpecified = NoDefaultSpecified() | ||||
class Undefined ( object ): pass | ||||
Undefined = Undefined() | ||||
Dav Clark
|
r2384 | class TraitError(Exception): | ||
pass | ||||
Brian Granger
|
r2157 | |||
#----------------------------------------------------------------------------- | ||||
# 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) | ||||
Dav Clark
|
r2384 | ['anytrait'] | ||
Brian Granger
|
r2175 | """ | ||
Brian Granger
|
r2157 | if isinstance(name, str): | ||
return [name] | ||||
elif name is None: | ||||
Dav Clark
|
r2384 | return ['anytrait'] | ||
Brian Granger
|
r2157 | elif isinstance(name, (list, tuple)): | ||
for n in name: | ||||
assert isinstance(n, str), "names must be strings" | ||||
return name | ||||
Brian Granger
|
r2184 | |||
class _SimpleTest: | ||||
def __init__ ( self, value ): self.value = value | ||||
def __call__ ( self, test ): | ||||
return test == self.value | ||||
def __repr__(self): | ||||
return "<SimpleTest(%r)" % self.value | ||||
def __str__(self): | ||||
return self.__repr__() | ||||
Brian Granger
|
r2287 | def getmembers(object, predicate=None): | ||
"""A safe version of inspect.getmembers that handles missing attributes. | ||||
This is useful when there are descriptor based attributes that for | ||||
some reason raise AttributeError even though they exist. This happens | ||||
in zope.inteface with the __provides__ attribute. | ||||
""" | ||||
results = [] | ||||
for key in dir(object): | ||||
try: | ||||
value = getattr(object, key) | ||||
except AttributeError: | ||||
pass | ||||
else: | ||||
if not predicate or predicate(value): | ||||
results.append((key, value)) | ||||
results.sort() | ||||
return results | ||||
Brian Granger
|
r2157 | #----------------------------------------------------------------------------- | ||
Dav Clark
|
r2385 | # Base TraitType for all traits | ||
Brian Granger
|
r2157 | #----------------------------------------------------------------------------- | ||
Dav Clark
|
r2385 | class TraitType(object): | ||
"""A base class for all trait descriptors. | ||||
Brian Granger
|
r2183 | |||
Notes | ||||
----- | ||||
Dav Clark
|
r2385 | Our implementation of traits is based on Python's descriptor | ||
Brian Granger
|
r2183 | prototol. This class is the base class for all such descriptors. The | ||
Dav Clark
|
r2384 | only magic we use is a custom metaclass for the main :class:`HasTraits` | ||
Brian Granger
|
r2183 | class that does the following: | ||
Dav Clark
|
r2385 | 1. Sets the :attr:`name` attribute of every :class:`TraitType` | ||
Brian Granger
|
r2183 | instance in the class dict to the name of the attribute. | ||
Dav Clark
|
r2385 | 2. Sets the :attr:`this_class` attribute of every :class:`TraitType` | ||
instance in the class dict to the *class* that declared the trait. | ||||
This is used by the :class:`This` trait to allow subclasses to | ||||
Brian Granger
|
r2183 | accept superclasses for :class:`This` values. | ||
""" | ||||
Brian Granger
|
r2157 | |||
metadata = {} | ||||
Brian Granger
|
r2177 | default_value = Undefined | ||
Brian Granger
|
r2157 | info_text = 'any value' | ||
def __init__(self, default_value=NoDefaultSpecified, **metadata): | ||||
Dav Clark
|
r2385 | """Create a TraitType. | ||
Brian Granger
|
r2177 | """ | ||
Brian Granger
|
r2157 | if default_value is not NoDefaultSpecified: | ||
self.default_value = default_value | ||||
Brian Granger
|
r2184 | |||
if len(metadata) > 0: | ||||
if len(self.metadata) > 0: | ||||
self._metadata = self.metadata.copy() | ||||
self._metadata.update(metadata) | ||||
else: | ||||
self._metadata = metadata | ||||
else: | ||||
self._metadata = self.metadata | ||||
Brian Granger
|
r2177 | self.init() | ||
Brian Granger
|
r2157 | |||
Brian Granger
|
r2177 | def init(self): | ||
pass | ||||
def get_default_value(self): | ||||
"""Create a new instance of the default value.""" | ||||
Brian Granger
|
r2742 | return self.default_value | ||
Brian Granger
|
r2177 | |||
Brian Granger
|
r2229 | def instance_init(self, obj): | ||
Dav Clark
|
r2384 | """This is called by :meth:`HasTraits.__new__` to finish init'ing. | ||
Brian Granger
|
r2229 | |||
Some stages of initialization must be delayed until the parent | ||||
Dav Clark
|
r2384 | :class:`HasTraits` instance has been created. This method is | ||
called in :meth:`HasTraits.__new__` after the instance has been | ||||
Brian Granger
|
r2229 | created. | ||
This method trigger the creation and validation of default values | ||||
and also things like the resolution of str given class names in | ||||
:class:`Type` and :class`Instance`. | ||||
Parameters | ||||
---------- | ||||
Dav Clark
|
r2384 | obj : :class:`HasTraits` instance | ||
The parent :class:`HasTraits` instance that has just been | ||||
Brian Granger
|
r2229 | created. | ||
""" | ||||
self.set_default_value(obj) | ||||
Brian Granger
|
r2182 | def set_default_value(self, obj): | ||
Brian Granger
|
r2229 | """Set the default value on a per instance basis. | ||
This method is called by :meth:`instance_init` to create and | ||||
validate the default value. The creation and validation of | ||||
Dav Clark
|
r2384 | default values must be delayed until the parent :class:`HasTraits` | ||
Brian Granger
|
r2229 | class has been instantiated. | ||
""" | ||||
Robert Kern
|
r3207 | # Check for a deferred initializer defined in the same class as the | ||
# trait declaration or above. | ||||
mro = type(obj).mro() | ||||
meth_name = '_%s_default' % self.name | ||||
for cls in mro[:mro.index(self.this_class)+1]: | ||||
if meth_name in cls.__dict__: | ||||
break | ||||
else: | ||||
# We didn't find one. Do static initialization. | ||||
dv = self.get_default_value() | ||||
newdv = self._validate(obj, dv) | ||||
obj._trait_values[self.name] = newdv | ||||
return | ||||
# Complete the dynamic initialization. | ||||
Robert Kern
|
r3336 | obj._trait_dyn_inits[self.name] = cls.__dict__[meth_name] | ||
Brian Granger
|
r2180 | |||
Brian Granger
|
r2182 | def __get__(self, obj, cls=None): | ||
Dav Clark
|
r2385 | """Get the value of the trait by self.name for the instance. | ||
Brian Granger
|
r2180 | |||
Dav Clark
|
r2384 | Default values are instantiated when :meth:`HasTraits.__new__` | ||
Brian Granger
|
r2182 | is called. Thus by the time this method gets called either the | ||
default value or a user defined value (they called :meth:`__set__`) | ||||
Dav Clark
|
r2384 | is in the :class:`HasTraits` instance. | ||
Brian Granger
|
r2177 | """ | ||
if obj is None: | ||||
Brian Granger
|
r2157 | return self | ||
else: | ||||
Brian Granger
|
r2182 | try: | ||
Dav Clark
|
r2385 | value = obj._trait_values[self.name] | ||
Robert Kern
|
r3207 | except KeyError: | ||
# Check for a dynamic initializer. | ||||
Robert Kern
|
r3336 | if self.name in obj._trait_dyn_inits: | ||
value = obj._trait_dyn_inits[self.name](obj) | ||||
Robert Kern
|
r3207 | # FIXME: Do we really validate here? | ||
value = self._validate(obj, value) | ||||
obj._trait_values[self.name] = value | ||||
return value | ||||
else: | ||||
raise TraitError('Unexpected error in TraitType: ' | ||||
'both default value and dynamic initializer are ' | ||||
'absent.') | ||||
except Exception: | ||||
Dav Clark
|
r2384 | # HasTraits should call set_default_value to populate | ||
Brian Granger
|
r2182 | # this. So this should never be reached. | ||
Dav Clark
|
r2385 | raise TraitError('Unexpected error in TraitType: ' | ||
Brian Granger
|
r2182 | 'default value not set properly') | ||
Brian Granger
|
r2177 | else: | ||
Brian Granger
|
r2182 | return value | ||
Brian Granger
|
r2177 | |||
Brian Granger
|
r2182 | def __set__(self, obj, value): | ||
Brian Granger
|
r2177 | new_value = self._validate(obj, value) | ||
Brian Granger
|
r2182 | old_value = self.__get__(obj) | ||
if old_value != new_value: | ||||
Dav Clark
|
r2385 | obj._trait_values[self.name] = new_value | ||
obj._notify_trait(self.name, old_value, new_value) | ||||
Brian Granger
|
r2157 | |||
Brian Granger
|
r2177 | def _validate(self, obj, value): | ||
Brian Granger
|
r2157 | if hasattr(self, 'validate'): | ||
Brian Granger
|
r2177 | return self.validate(obj, value) | ||
Brian Granger
|
r2157 | elif hasattr(self, 'is_valid_for'): | ||
valid = self.is_valid_for(value) | ||||
if valid: | ||||
return value | ||||
else: | ||||
Dav Clark
|
r2385 | raise TraitError('invalid value for type: %r' % value) | ||
Brian Granger
|
r2157 | 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: | ||||
Dav Clark
|
r2385 | e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \ | ||
Brian Granger
|
r2157 | % (self.name, class_of(obj), | ||
self.info(), repr_type(value)) | ||||
else: | ||||
Dav Clark
|
r2385 | e = "The '%s' trait must be %s, but a value of %r was specified." \ | ||
Brian Granger
|
r2157 | % (self.name, self.info(), repr_type(value)) | ||
Dav Clark
|
r2385 | raise TraitError(e) | ||
Brian Granger
|
r2157 | |||
Brian Granger
|
r2184 | def get_metadata(self, key): | ||
return getattr(self, '_metadata', {}).get(key, None) | ||||
def set_metadata(self, key, value): | ||||
getattr(self, '_metadata', {})[key] = value | ||||
Brian Granger
|
r2157 | |||
#----------------------------------------------------------------------------- | ||||
Dav Clark
|
r2384 | # The HasTraits implementation | ||
Brian Granger
|
r2157 | #----------------------------------------------------------------------------- | ||
Dav Clark
|
r2384 | class MetaHasTraits(type): | ||
"""A metaclass for HasTraits. | ||||
Brian Granger
|
r2157 | |||
Dav Clark
|
r2385 | This metaclass makes sure that any TraitType class attributes are | ||
Brian Granger
|
r2157 | instantiated and sets their name attribute. | ||
""" | ||||
def __new__(mcls, name, bases, classdict): | ||||
Dav Clark
|
r2384 | """Create the HasTraits class. | ||
Brian Granger
|
r2183 | |||
Dav Clark
|
r2385 | This instantiates all TraitTypes in the class dict and sets their | ||
Brian Granger
|
r2183 | :attr:`name` attribute. | ||
""" | ||||
Brian Granger
|
r2287 | # print "MetaHasTraitlets (mcls, name): ", mcls, name | ||
# print "MetaHasTraitlets (bases): ", bases | ||||
# print "MetaHasTraitlets (classdict): ", classdict | ||||
Brian Granger
|
r2157 | for k,v in classdict.iteritems(): | ||
Dav Clark
|
r2385 | if isinstance(v, TraitType): | ||
Brian Granger
|
r2157 | v.name = k | ||
elif inspect.isclass(v): | ||||
Dav Clark
|
r2385 | if issubclass(v, TraitType): | ||
Brian Granger
|
r2157 | vinst = v() | ||
vinst.name = k | ||||
classdict[k] = vinst | ||||
Dav Clark
|
r2384 | return super(MetaHasTraits, mcls).__new__(mcls, name, bases, classdict) | ||
Brian Granger
|
r2157 | |||
Brian Granger
|
r2183 | def __init__(cls, name, bases, classdict): | ||
Dav Clark
|
r2384 | """Finish initializing the HasTraits class. | ||
Brian Granger
|
r2183 | |||
Dav Clark
|
r2385 | This sets the :attr:`this_class` attribute of each TraitType in the | ||
Brian Granger
|
r2183 | class dict to the newly created class ``cls``. | ||
""" | ||||
for k, v in classdict.iteritems(): | ||||
Dav Clark
|
r2385 | if isinstance(v, TraitType): | ||
Brian Granger
|
r2183 | v.this_class = cls | ||
Dav Clark
|
r2384 | super(MetaHasTraits, cls).__init__(name, bases, classdict) | ||
Brian Granger
|
r2157 | |||
Dav Clark
|
r2384 | class HasTraits(object): | ||
Brian Granger
|
r2157 | |||
Dav Clark
|
r2384 | __metaclass__ = MetaHasTraits | ||
Brian Granger
|
r2157 | |||
Brian Granger
|
r2739 | def __new__(cls, **kw): | ||
Brian Granger
|
r2255 | # This is needed because in Python 2.6 object.__new__ only accepts | ||
# the cls argument. | ||||
Dav Clark
|
r2384 | new_meth = super(HasTraits, cls).__new__ | ||
Brian Granger
|
r2255 | if new_meth is object.__new__: | ||
inst = new_meth(cls) | ||||
else: | ||||
Brian Granger
|
r2739 | inst = new_meth(cls, **kw) | ||
Dav Clark
|
r2385 | inst._trait_values = {} | ||
inst._trait_notifiers = {} | ||||
Robert Kern
|
r3336 | inst._trait_dyn_inits = {} | ||
Dav Clark
|
r2385 | # Here we tell all the TraitType instances to set their default | ||
Brian Granger
|
r2182 | # values on the instance. | ||
for key in dir(cls): | ||||
Brian Granger
|
r2287 | # Some descriptors raise AttributeError like zope.interface's | ||
# __provides__ attributes even though they exist. This causes | ||||
# AttributeErrors even though they are listed in dir(cls). | ||||
try: | ||||
value = getattr(cls, key) | ||||
except AttributeError: | ||||
pass | ||||
else: | ||||
Dav Clark
|
r2469 | if isinstance(value, TraitType): | ||
Brian Granger
|
r2287 | value.instance_init(inst) | ||
Dav Clark
|
r2469 | |||
Brian Granger
|
r2182 | return inst | ||
Brian Granger
|
r2740 | def __init__(self, **kw): | ||
# Allow trait values to be set using keyword arguments. | ||||
Brian Granger
|
r2745 | # We need to use setattr for this to trigger validation and | ||
# notifications. | ||||
Brian Granger
|
r2739 | for key, value in kw.iteritems(): | ||
setattr(self, key, value) | ||||
Brian Granger
|
r2157 | |||
Dav Clark
|
r2385 | def _notify_trait(self, name, old_value, new_value): | ||
Brian Granger
|
r2175 | |||
# First dynamic ones | ||||
Dav Clark
|
r2385 | callables = self._trait_notifiers.get(name,[]) | ||
more_callables = self._trait_notifiers.get('anytrait',[]) | ||||
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: | ||||
Dav Clark
|
r2385 | raise TraitError('a trait changed callback ' | ||
Brian Granger
|
r2175 | 'must have 0-3 arguments.') | ||
else: | ||||
Dav Clark
|
r2385 | raise TraitError('a trait changed callback ' | ||
Brian Granger
|
r2175 | 'must be callable.') | ||
Brian Granger
|
r2157 | |||
def _add_notifiers(self, handler, name): | ||||
Dav Clark
|
r2385 | if not self._trait_notifiers.has_key(name): | ||
Brian Granger
|
r2157 | nlist = [] | ||
Dav Clark
|
r2385 | self._trait_notifiers[name] = nlist | ||
Brian Granger
|
r2157 | else: | ||
Dav Clark
|
r2385 | nlist = self._trait_notifiers[name] | ||
Brian Granger
|
r2157 | if handler not in nlist: | ||
nlist.append(handler) | ||||
def _remove_notifiers(self, handler, name): | ||||
Dav Clark
|
r2385 | if self._trait_notifiers.has_key(name): | ||
nlist = self._trait_notifiers[name] | ||||
Brian Granger
|
r2157 | try: | ||
index = nlist.index(handler) | ||||
except ValueError: | ||||
pass | ||||
else: | ||||
del nlist[index] | ||||
Dav Clark
|
r2384 | def on_trait_change(self, handler, name=None, remove=False): | ||
Dav Clark
|
r2385 | """Setup a handler to be called when a trait changes. | ||
Brian Granger
|
r2175 | |||
Dav Clark
|
r2385 | This is used to setup dynamic notifications of trait changes. | ||
Brian Granger
|
r2175 | |||
Dav Clark
|
r2384 | Static handlers can be created by creating methods on a HasTraits | ||
Dav Clark
|
r2385 | subclass with the naming convention '_[traitname]_changed'. Thus, | ||
to create static handler for the trait 'a', create the method | ||||
Brian Granger
|
r2175 | _a_changed(self, name, old, new) (fewer arguments can be used, see | ||
below). | ||||
Parameters | ||||
---------- | ||||
Brian Granger
|
r2277 | handler : callable | ||
Dav Clark
|
r2385 | A callable that is called when a trait changes. Its | ||
Brian Granger
|
r2277 | signature can be handler(), handler(name), handler(name, new) | ||
or handler(name, old, new). | ||||
name : list, str, None | ||||
Dav Clark
|
r2385 | If None, the handler will apply to all traits. If a list | ||
Brian Granger
|
r2277 | 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
|
r2175 | """ | ||
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) | ||||
Dav Clark
|
r2384 | def trait_names(self, **metadata): | ||
Dav Clark
|
r2385 | """Get a list of all the names of this classes traits.""" | ||
Dav Clark
|
r2384 | return self.traits(**metadata).keys() | ||
Brian Granger
|
r2184 | |||
Dav Clark
|
r2384 | def traits(self, **metadata): | ||
Dav Clark
|
r2385 | """Get a list of all the traits of this class. | ||
Brian Granger
|
r2184 | |||
Dav Clark
|
r2385 | The TraitTypes returned don't know anything about the values | ||
that the various HasTrait's instances are holding. | ||||
Brian Granger
|
r2245 | |||
This follows the same algorithm as traits does and does not allow | ||||
for any simple way of specifying merely that a metadata name | ||||
exists, but has any value. This is because get_metadata returns | ||||
None if a metadata key doesn't exist. | ||||
Brian Granger
|
r2184 | """ | ||
Dav Clark
|
r2469 | traits = dict([memb for memb in getmembers(self.__class__) if \ | ||
Dav Clark
|
r2385 | isinstance(memb[1], TraitType)]) | ||
Brian Granger
|
r2184 | |||
Brian Granger
|
r2245 | if len(metadata) == 0: | ||
Dav Clark
|
r2385 | return traits | ||
Brian Granger
|
r2200 | |||
Brian Granger
|
r2184 | for meta_name, meta_eval in metadata.items(): | ||
if type(meta_eval) is not FunctionType: | ||||
metadata[meta_name] = _SimpleTest(meta_eval) | ||||
result = {} | ||||
Dav Clark
|
r2385 | for name, trait in traits.items(): | ||
Brian Granger
|
r2184 | for meta_name, meta_eval in metadata.items(): | ||
Dav Clark
|
r2385 | if not meta_eval(trait.get_metadata(meta_name)): | ||
Brian Granger
|
r2184 | break | ||
else: | ||||
Dav Clark
|
r2385 | result[name] = trait | ||
Brian Granger
|
r2184 | |||
return result | ||||
Brian Granger
|
r2179 | |||
Dav Clark
|
r2385 | def trait_metadata(self, traitname, key): | ||
"""Get metadata values for trait by key.""" | ||||
Brian Granger
|
r2184 | try: | ||
Dav Clark
|
r2385 | trait = getattr(self.__class__, traitname) | ||
Brian Granger
|
r2184 | except AttributeError: | ||
Dav Clark
|
r2385 | raise TraitError("Class %s does not have a trait named %s" % | ||
(self.__class__.__name__, traitname)) | ||||
Brian Granger
|
r2184 | else: | ||
Dav Clark
|
r2385 | return trait.get_metadata(key) | ||
Brian Granger
|
r2177 | |||
Brian Granger
|
r2157 | #----------------------------------------------------------------------------- | ||
Dav Clark
|
r2385 | # Actual TraitTypes implementations/subclasses | ||
Brian Granger
|
r2157 | #----------------------------------------------------------------------------- | ||
Brian Granger
|
r2177 | |||
#----------------------------------------------------------------------------- | ||||
Dav Clark
|
r2385 | # TraitTypes subclasses for handling classes and instances of classes | ||
Brian Granger
|
r2177 | #----------------------------------------------------------------------------- | ||
Dav Clark
|
r2385 | class ClassBasedTraitType(TraitType): | ||
"""A trait with error reporting for Type, Instance and This.""" | ||||
Brian Granger
|
r2177 | |||
Brian Granger
|
r2182 | def error(self, obj, value): | ||
Brian Granger
|
r2177 | kind = type(value) | ||
if kind is InstanceType: | ||||
msg = 'class %s' % value.__class__.__name__ | ||||
else: | ||||
msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) ) | ||||
Dav Clark
|
r2385 | super(ClassBasedTraitType, self).error(obj, msg) | ||
Brian Granger
|
r2177 | |||
Dav Clark
|
r2385 | class Type(ClassBasedTraitType): | ||
"""A trait whose value must be a subclass of a specified class.""" | ||||
Brian Granger
|
r2177 | |||
def __init__ (self, default_value=None, klass=None, allow_none=True, **metadata ): | ||||
Dav Clark
|
r2385 | """Construct a Type trait | ||
Brian Granger
|
r2177 | |||
Dav Clark
|
r2385 | A Type trait specifies that its values must be subclasses of | ||
Brian Granger
|
r2177 | a particular class. | ||
Brian Granger
|
r2229 | If only ``default_value`` is given, it is used for the ``klass`` as | ||
well. | ||||
Brian Granger
|
r2177 | Parameters | ||
---------- | ||||
Brian Granger
|
r2229 | default_value : class, str or None | ||
The default value must be a subclass of klass. If an str, | ||||
the str must be a fully specified class name, like 'foo.bar.Bah'. | ||||
The string is resolved into real class, when the parent | ||||
Dav Clark
|
r2384 | :class:`HasTraits` class is instantiated. | ||
Brian Granger
|
r2177 | klass : class, str, None | ||
Dav Clark
|
r2385 | Values of this trait must be a subclass of klass. The klass | ||
Brian Granger
|
r2177 | may be specified in a string like: 'foo.bar.MyClass'. | ||
Brian Granger
|
r2229 | The string is resolved into real class, when the parent | ||
Dav Clark
|
r2384 | :class:`HasTraits` class is instantiated. | ||
Brian Granger
|
r2177 | allow_none : boolean | ||
Indicates whether None is allowed as an assignable value. Even if | ||||
``False``, the default value may be ``None``. | ||||
""" | ||||
if default_value is None: | ||||
if klass is None: | ||||
klass = object | ||||
elif klass is None: | ||||
klass = default_value | ||||
Brian Granger
|
r2229 | if not (inspect.isclass(klass) or isinstance(klass, basestring)): | ||
Dav Clark
|
r2385 | raise TraitError("A Type trait must specify a class.") | ||
Brian Granger
|
r2177 | |||
self.klass = klass | ||||
self._allow_none = allow_none | ||||
super(Type, self).__init__(default_value, **metadata) | ||||
def validate(self, obj, value): | ||||
"""Validates that the value is a valid object instance.""" | ||||
try: | ||||
if issubclass(value, self.klass): | ||||
return value | ||||
except: | ||||
if (value is None) and (self._allow_none): | ||||
return value | ||||
self.error(obj, value) | ||||
def info(self): | ||||
""" Returns a description of the trait.""" | ||||
Brian Granger
|
r2229 | if isinstance(self.klass, basestring): | ||
klass = self.klass | ||||
else: | ||||
klass = self.klass.__name__ | ||||
Brian Granger
|
r2177 | result = 'a subclass of ' + klass | ||
if self._allow_none: | ||||
return result + ' or None' | ||||
return result | ||||
Brian Granger
|
r2229 | def instance_init(self, obj): | ||
self._resolve_classes() | ||||
super(Type, self).instance_init(obj) | ||||
def _resolve_classes(self): | ||||
if isinstance(self.klass, basestring): | ||||
self.klass = import_item(self.klass) | ||||
if isinstance(self.default_value, basestring): | ||||
self.default_value = import_item(self.default_value) | ||||
def get_default_value(self): | ||||
return self.default_value | ||||
Brian Granger
|
r2177 | |||
class DefaultValueGenerator(object): | ||||
"""A class for generating new default value instances.""" | ||||
Brian Granger
|
r2229 | def __init__(self, *args, **kw): | ||
Brian Granger
|
r2177 | self.args = args | ||
self.kw = kw | ||||
Brian Granger
|
r2229 | def generate(self, klass): | ||
return klass(*self.args, **self.kw) | ||||
Brian Granger
|
r2177 | |||
Brian Granger
|
r2182 | |||
Dav Clark
|
r2385 | class Instance(ClassBasedTraitType): | ||
Brian Granger
|
r2177 | """A trait whose value must be an instance of a specified class. | ||
The value can also be an instance of a subclass of the specified class. | ||||
""" | ||||
Brian Granger
|
r2182 | def __init__(self, klass=None, args=None, kw=None, | ||
allow_none=True, **metadata ): | ||||
Dav Clark
|
r2385 | """Construct an Instance trait. | ||
Brian Granger
|
r2177 | |||
Dav Clark
|
r2385 | This trait allows values that are instances of a particular | ||
Brian Granger
|
r2182 | class or its sublclasses. Our implementation is quite different | ||
from that of enthough.traits as we don't allow instances to be used | ||||
for klass and we handle the ``args`` and ``kw`` arguments differently. | ||||
Brian Granger
|
r2177 | Parameters | ||
---------- | ||||
Brian Granger
|
r2229 | klass : class, str | ||
Dav Clark
|
r2385 | The class that forms the basis for the trait. Class names | ||
Brian Granger
|
r2229 | can also be specified as strings, like 'foo.bar.Bar'. | ||
Brian Granger
|
r2177 | args : tuple | ||
Positional arguments for generating the default value. | ||||
kw : dict | ||||
Keyword arguments for generating the default value. | ||||
allow_none : bool | ||||
Indicates whether None is allowed as a value. | ||||
Default Value | ||||
------------- | ||||
Brian Granger
|
r2182 | If both ``args`` and ``kw`` are None, then the default value is None. | ||
If ``args`` is a tuple and ``kw`` is a dict, then the default is | ||||
created as ``klass(*args, **kw)``. If either ``args`` or ``kw`` is | ||||
not (but not both), None is replace by ``()`` or ``{}``. | ||||
Brian Granger
|
r2177 | """ | ||
self._allow_none = allow_none | ||||
Brian Granger
|
r2182 | |||
Brian Granger
|
r2229 | if (klass is None) or (not (inspect.isclass(klass) or isinstance(klass, basestring))): | ||
Dav Clark
|
r2385 | raise TraitError('The klass argument must be a class' | ||
Brian Granger
|
r2182 | ' you gave: %r' % klass) | ||
self.klass = klass | ||||
# self.klass is a class, so handle default_value | ||||
if args is None and kw is None: | ||||
Brian Granger
|
r2177 | default_value = None | ||
else: | ||||
if args is None: | ||||
Brian Granger
|
r2182 | # kw is not None | ||
Brian Granger
|
r2177 | args = () | ||
Brian Granger
|
r2182 | elif kw is None: | ||
# args is not None | ||||
kw = {} | ||||
Brian Granger
|
r2177 | if not isinstance(kw, dict): | ||
Dav Clark
|
r2385 | raise TraitError("The 'kw' argument must be a dict or None.") | ||
Brian Granger
|
r2177 | if not isinstance(args, tuple): | ||
Dav Clark
|
r2385 | raise TraitError("The 'args' argument must be a tuple or None.") | ||
Brian Granger
|
r2177 | |||
Brian Granger
|
r2229 | default_value = DefaultValueGenerator(*args, **kw) | ||
Brian Granger
|
r2177 | |||
super(Instance, self).__init__(default_value, **metadata) | ||||
def validate(self, obj, value): | ||||
if value is None: | ||||
if self._allow_none: | ||||
return value | ||||
Brian Granger
|
r2182 | self.error(obj, value) | ||
Brian Granger
|
r2177 | |||
if isinstance(value, self.klass): | ||||
return value | ||||
else: | ||||
Brian Granger
|
r2182 | self.error(obj, value) | ||
Brian Granger
|
r2177 | |||
Brian Granger
|
r2182 | def info(self): | ||
Brian Granger
|
r2229 | if isinstance(self.klass, basestring): | ||
klass = self.klass | ||||
else: | ||||
klass = self.klass.__name__ | ||||
Brian Granger
|
r2177 | result = class_of(klass) | ||
if self._allow_none: | ||||
return result + ' or None' | ||||
return result | ||||
Brian Granger
|
r2229 | def instance_init(self, obj): | ||
self._resolve_classes() | ||||
super(Instance, self).instance_init(obj) | ||||
def _resolve_classes(self): | ||||
if isinstance(self.klass, basestring): | ||||
self.klass = import_item(self.klass) | ||||
Brian Granger
|
r2182 | def get_default_value(self): | ||
Brian Granger
|
r2177 | """Instantiate a default value instance. | ||
Dav Clark
|
r2384 | This is called when the containing HasTraits classes' | ||
Brian Granger
|
r2182 | :meth:`__new__` method is called to ensure that a unique instance | ||
Dav Clark
|
r2384 | is created for each HasTraits instance. | ||
Brian Granger
|
r2177 | """ | ||
dv = self.default_value | ||||
if isinstance(dv, DefaultValueGenerator): | ||||
Brian Granger
|
r2229 | return dv.generate(self.klass) | ||
Brian Granger
|
r2177 | else: | ||
return dv | ||||
Dav Clark
|
r2385 | class This(ClassBasedTraitType): | ||
"""A trait for instances of the class containing this trait. | ||||
Brian Granger
|
r2180 | |||
Brian Granger
|
r2182 | Because how how and when class bodies are executed, the ``This`` | ||
Dav Clark
|
r2385 | trait can only have a default value of None. This, and because we | ||
Brian Granger
|
r2182 | always validate default values, ``allow_none`` is *always* true. | ||
""" | ||||
Brian Granger
|
r2180 | |||
Brian Granger
|
r2182 | info_text = 'an instance of the same type as the receiver or None' | ||
Brian Granger
|
r2180 | |||
Brian Granger
|
r2182 | def __init__(self, **metadata): | ||
super(This, self).__init__(None, **metadata) | ||||
Brian Granger
|
r2180 | |||
Brian Granger
|
r2182 | def validate(self, obj, value): | ||
Brian Granger
|
r2183 | # What if value is a superclass of obj.__class__? This is | ||
# complicated if it was the superclass that defined the This | ||||
Dav Clark
|
r2385 | # trait. | ||
Brian Granger
|
r2183 | if isinstance(value, self.this_class) or (value is None): | ||
Brian Granger
|
r2180 | return value | ||
else: | ||||
Brian Granger
|
r2182 | self.error(obj, value) | ||
Brian Granger
|
r2180 | |||
Brian Granger
|
r2177 | #----------------------------------------------------------------------------- | ||
Dav Clark
|
r2385 | # Basic TraitTypes implementations/subclasses | ||
Brian Granger
|
r2177 | #----------------------------------------------------------------------------- | ||
Brian Granger
|
r2157 | |||
Dav Clark
|
r2385 | class Any(TraitType): | ||
Brian Granger
|
r2157 | default_value = None | ||
info_text = 'any value' | ||||
Dav Clark
|
r2385 | class Int(TraitType): | ||
"""A integer trait.""" | ||||
Brian Granger
|
r2157 | |||
default_value = 0 | ||||
info_text = 'an integer' | ||||
def validate(self, obj, value): | ||||
if isinstance(value, int): | ||||
return value | ||||
self.error(obj, value) | ||||
Brian Granger
|
r2178 | class CInt(Int): | ||
Dav Clark
|
r2385 | """A casting version of the int trait.""" | ||
Brian Granger
|
r2178 | |||
def validate(self, obj, value): | ||||
try: | ||||
return int(value) | ||||
except: | ||||
self.error(obj, value) | ||||
Brian Granger
|
r2157 | |||
Dav Clark
|
r2385 | class Long(TraitType): | ||
"""A long integer trait.""" | ||||
Brian Granger
|
r2157 | |||
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) | ||||
Brian Granger
|
r2178 | class CLong(Long): | ||
Dav Clark
|
r2385 | """A casting version of the long integer trait.""" | ||
Brian Granger
|
r2178 | |||
def validate(self, obj, value): | ||||
try: | ||||
return long(value) | ||||
except: | ||||
self.error(obj, value) | ||||
Dav Clark
|
r2385 | class Float(TraitType): | ||
"""A float trait.""" | ||||
Brian Granger
|
r2157 | |||
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) | ||||
Brian Granger
|
r2178 | class CFloat(Float): | ||
Dav Clark
|
r2385 | """A casting version of the float trait.""" | ||
Brian Granger
|
r2178 | |||
def validate(self, obj, value): | ||||
try: | ||||
return float(value) | ||||
except: | ||||
self.error(obj, value) | ||||
Dav Clark
|
r2385 | class Complex(TraitType): | ||
"""A trait for complex numbers.""" | ||||
Brian Granger
|
r2157 | |||
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) | ||||
Brian Granger
|
r2178 | class CComplex(Complex): | ||
Dav Clark
|
r2385 | """A casting version of the complex number trait.""" | ||
Brian Granger
|
r2178 | |||
def validate (self, obj, value): | ||||
try: | ||||
return complex(value) | ||||
except: | ||||
self.error(obj, value) | ||||
Dav Clark
|
r2385 | class Str(TraitType): | ||
"""A trait for strings.""" | ||||
Brian Granger
|
r2157 | |||
default_value = '' | ||||
info_text = 'a string' | ||||
def validate(self, obj, value): | ||||
if isinstance(value, str): | ||||
return value | ||||
self.error(obj, value) | ||||
Brian Granger
|
r2178 | class CStr(Str): | ||
Dav Clark
|
r2385 | """A casting version of the string trait.""" | ||
Brian Granger
|
r2178 | |||
def validate(self, obj, value): | ||||
try: | ||||
return str(value) | ||||
except: | ||||
try: | ||||
return unicode(value) | ||||
except: | ||||
self.error(obj, value) | ||||
Dav Clark
|
r2385 | class Unicode(TraitType): | ||
"""A trait for unicode strings.""" | ||||
Brian Granger
|
r2157 | |||
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) | ||||
Brian Granger
|
r2178 | class CUnicode(Unicode): | ||
Dav Clark
|
r2385 | """A casting version of the unicode trait.""" | ||
Brian Granger
|
r2178 | |||
def validate(self, obj, value): | ||||
try: | ||||
return unicode(value) | ||||
except: | ||||
self.error(obj, value) | ||||
Brian Granger
|
r2157 | |||
Dav Clark
|
r2385 | class Bool(TraitType): | ||
"""A boolean (True, False) trait.""" | ||||
Brian Granger
|
r2742 | |||
Brian Granger
|
r2157 | default_value = False | ||
info_text = 'a boolean' | ||||
def validate(self, obj, value): | ||||
if isinstance(value, bool): | ||||
return value | ||||
self.error(obj, value) | ||||
Brian Granger
|
r2178 | |||
class CBool(Bool): | ||||
Dav Clark
|
r2385 | """A casting version of the boolean trait.""" | ||
Brian Granger
|
r2178 | |||
def validate(self, obj, value): | ||||
try: | ||||
return bool(value) | ||||
except: | ||||
Brian Granger
|
r2203 | self.error(obj, value) | ||
Brian Granger
|
r2204 | |||
Dav Clark
|
r2385 | class Enum(TraitType): | ||
Brian Granger
|
r2204 | """An enum that whose value must be in a given sequence.""" | ||
Brian Granger
|
r2203 | |||
def __init__(self, values, default_value=None, allow_none=True, **metadata): | ||||
self.values = values | ||||
self._allow_none = allow_none | ||||
super(Enum, self).__init__(default_value, **metadata) | ||||
def validate(self, obj, value): | ||||
if value is None: | ||||
if self._allow_none: | ||||
return value | ||||
if value in self.values: | ||||
return value | ||||
self.error(obj, value) | ||||
def info(self): | ||||
""" Returns a description of the trait.""" | ||||
result = 'any of ' + repr(self.values) | ||||
if self._allow_none: | ||||
return result + ' or None' | ||||
return result | ||||
class CaselessStrEnum(Enum): | ||||
Brian Granger
|
r2204 | """An enum of strings that are caseless in validate.""" | ||
Brian Granger
|
r2203 | |||
def validate(self, obj, value): | ||||
if value is None: | ||||
if self._allow_none: | ||||
return value | ||||
if not isinstance(value, str): | ||||
self.error(obj, value) | ||||
for v in self.values: | ||||
if v.lower() == value.lower(): | ||||
return v | ||||
Brian Granger
|
r2204 | self.error(obj, value) | ||
class List(Instance): | ||||
"""An instance of a Python list.""" | ||||
def __init__(self, default_value=None, allow_none=True, **metadata): | ||||
Dav Clark
|
r2385 | """Create a list trait type from a list or tuple. | ||
Brian Granger
|
r2204 | |||
The default value is created by doing ``list(default_value)``, | ||||
which creates a copy of the ``default_value``. | ||||
""" | ||||
if default_value is None: | ||||
args = ((),) | ||||
elif isinstance(default_value, SequenceTypes): | ||||
args = (default_value,) | ||||
Dav Clark
|
r2380 | else: | ||
raise TypeError('default value of List was %s' % default_value) | ||||
Brian Granger
|
r2204 | |||
super(List,self).__init__(klass=list, args=args, | ||||
allow_none=allow_none, **metadata) | ||||
Brian Granger
|
r2742 | |||
Brian Granger
|
r2738 | class Dict(Instance): | ||
"""An instance of a Python dict.""" | ||||
def __init__(self, default_value=None, allow_none=True, **metadata): | ||||
"""Create a dict trait type from a dict. | ||||
The default value is created by doing ``dict(default_value)``, | ||||
which creates a copy of the ``default_value``. | ||||
""" | ||||
if default_value is None: | ||||
args = ((),) | ||||
elif isinstance(default_value, dict): | ||||
args = (default_value,) | ||||
elif isinstance(default_value, SequenceTypes): | ||||
args = (default_value,) | ||||
else: | ||||
raise TypeError('default value of Dict was %s' % default_value) | ||||
super(Dict,self).__init__(klass=dict, args=args, | ||||
allow_none=allow_none, **metadata) | ||||
Brian Granger
|
r2752 | |||
Brian Granger
|
r2742 | class TCPAddress(TraitType): | ||
"""A trait for an (ip, port) tuple. | ||||
This allows for both IPv4 IP addresses as well as hostnames. | ||||
""" | ||||
default_value = ('127.0.0.1', 0) | ||||
info_text = 'an (ip, port) tuple' | ||||
def validate(self, obj, value): | ||||
if isinstance(value, tuple): | ||||
if len(value) == 2: | ||||
if isinstance(value[0], basestring) and isinstance(value[1], int): | ||||
port = value[1] | ||||
if port >= 0 and port <= 65535: | ||||
return value | ||||
self.error(obj, value) | ||||