From 5ffdabae62255daa2b312f124c97e7880e7b0f7e 2015-03-29 23:28:51 From: Thomas Kluyver Date: 2015-03-29 23:28:51 Subject: [PATCH] Merge pull request #8188 from minrk/bigsplit-traitlets Big Split: traitlets, config --- diff --git a/IPython/config.py b/IPython/config.py new file mode 100644 index 0000000..fd035f2 --- /dev/null +++ b/IPython/config.py @@ -0,0 +1,18 @@ +""" +Shim to maintain backwards compatibility with old IPython.config imports. +""" +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +import sys +from warnings import warn + +warn("The `IPython.config` package has been deprecated. " + "You should import from traitlets.config instead.") + +from IPython.utils.shimmodule import ShimModule + +# Unconditionally insert the shim into sys.modules so that further import calls +# trigger the custom attribute access above + +sys.modules['IPython.config'] = ShimModule(src='IPython.config', mirror='traitlets.config') diff --git a/IPython/html/widgets/tests/test_traits.py b/IPython/html/widgets/tests/test_traits.py index c25960a..bb74949 100644 --- a/IPython/html/widgets/tests/test_traits.py +++ b/IPython/html/widgets/tests/test_traits.py @@ -5,7 +5,7 @@ from unittest import TestCase from IPython.utils.traitlets import HasTraits -from IPython.utils.tests.test_traitlets import TraitTestBase +from traitlets.tests.test_traitlets import TraitTestBase from IPython.html.widgets import Color diff --git a/IPython/testing/iptest.py b/IPython/testing/iptest.py index d84fab5..b064d80 100644 --- a/IPython/testing/iptest.py +++ b/IPython/testing/iptest.py @@ -174,6 +174,7 @@ shims = { 'parallel': 'ipython_parallel', 'kernel': 'ipython_kernel', 'kernel.inprocess': 'ipython_kernel.inprocess', + 'config': 'traitlets', } # Name -> (include, exclude, dependencies_met) @@ -270,10 +271,6 @@ if not have['pygments'] or not have['jinja2']: if not have['terminado']: sec.exclude('terminal') -# config: -# Config files aren't really importable stand-alone -test_sections['config'].exclude('profile') - # nbconvert: sec = test_sections['nbconvert'] sec.requires('pygments', 'jinja2', 'jsonschema', 'mistune') diff --git a/IPython/testing/tools.py b/IPython/testing/tools.py index cfb4c9f..b92489c 100644 --- a/IPython/testing/tools.py +++ b/IPython/testing/tools.py @@ -217,7 +217,10 @@ def ipexec(fname, options=None, commands=()): full_fname = os.path.join(test_dir, fname) full_cmd = ipython_cmd + cmdargs + [full_fname] env = os.environ.copy() - env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr + # FIXME: ignore all warnings in ipexec while we have shims + # should we keep suppressing warnings here, even after removing shims? + env['PYTHONWARNINGS'] = 'ignore' + # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr for k, v in env.items(): # Debug a bizarre failure we've seen on Windows: # TypeError: environment can only contain strings diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index 92fff44..84b16da 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -1,1874 +1,3 @@ -# encoding: utf-8 -""" -A lightweight Traits like module. +from __future__ import absolute_import -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. Most importantly, we don't provide container - traits (list, dict, tuple) that can trigger notifications if their - contents change. -* API compatibility with enthought.traits - -There are also some important difference in our design: - -* enthought.traits does not validate default values. We do. - -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. - -Inheritance diagram: - -.. inheritance-diagram:: IPython.utils.traitlets - :parts: 3 -""" - -# Copyright (c) IPython Development Team. -# Distributed under the terms of the Modified BSD License. -# -# Adapted from enthought.traits, Copyright (c) Enthought, Inc., -# also under the terms of the Modified BSD License. - -import contextlib -import inspect -import re -import sys -import types -from types import FunctionType -try: - from types import ClassType, InstanceType - ClassTypes = (ClassType, type) -except: - ClassTypes = (type,) -from warnings import warn - -from .getargspec import getargspec -from .importstring import import_item -from IPython.utils import py3compat -from IPython.utils import eventful -from IPython.utils.py3compat import iteritems, string_types -from IPython.testing.skipdoctest import skip_doctest - -SequenceTypes = (list, tuple, set, frozenset) - -#----------------------------------------------------------------------------- -# Basic classes -#----------------------------------------------------------------------------- - - -class NoDefaultSpecified ( object ): pass -NoDefaultSpecified = NoDefaultSpecified() - - -class Undefined ( object ): pass -Undefined = Undefined() - -class TraitError(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, py3compat.string_types ): - 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 (not py3compat.PY3) and the_type is InstanceType: - # Old-style class. - the_type = obj.__class__ - msg = '%r %r' % (obj, the_type) - return msg - - -def is_trait(t): - """ Returns whether the given value is an instance or subclass of TraitType. - """ - return (isinstance(t, TraitType) or - (isinstance(t, type) and issubclass(t, TraitType))) - - -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) - ['anytrait'] - """ - if isinstance(name, string_types): - return [name] - elif name is None: - return ['anytrait'] - elif isinstance(name, (list, tuple)): - for n in name: - assert isinstance(n, string_types), "names must be strings" - return name - - -class _SimpleTest: - def __init__ ( self, value ): self.value = value - def __call__ ( self, test ): - return test == self.value - def __repr__(self): - return ">> c = link((obj1, 'value'), (obj2, 'value'), (obj3, 'value')) - >>> obj1.value = 5 # updates other objects as well - """ - updating = False - def __init__(self, *args): - if len(args) < 2: - raise TypeError('At least two traitlets must be provided.') - _validate_link(*args) - - self.objects = {} - - initial = getattr(args[0][0], args[0][1]) - for obj, attr in args: - setattr(obj, attr, initial) - - callback = self._make_closure(obj, attr) - obj.on_trait_change(callback, attr) - self.objects[(obj, attr)] = callback - - @contextlib.contextmanager - def _busy_updating(self): - self.updating = True - try: - yield - finally: - self.updating = False - - def _make_closure(self, sending_obj, sending_attr): - def update(name, old, new): - self._update(sending_obj, sending_attr, new) - return update - - def _update(self, sending_obj, sending_attr, new): - if self.updating: - return - with self._busy_updating(): - for obj, attr in self.objects.keys(): - setattr(obj, attr, new) - - def unlink(self): - for key, callback in self.objects.items(): - (obj, attr) = key - obj.on_trait_change(callback, attr, remove=True) - -@skip_doctest -class directional_link(object): - """Link the trait of a source object with traits of target objects. - - Parameters - ---------- - source : pair of object, name - targets : pairs of objects/attributes - - Examples - -------- - - >>> c = directional_link((src, 'value'), (tgt1, 'value'), (tgt2, 'value')) - >>> src.value = 5 # updates target objects - >>> tgt1.value = 6 # does not update other objects - """ - updating = False - - def __init__(self, source, *targets): - if len(targets) < 1: - raise TypeError('At least two traitlets must be provided.') - _validate_link(source, *targets) - self.source = source - self.targets = targets - - # Update current value - src_attr_value = getattr(source[0], source[1]) - for obj, attr in targets: - setattr(obj, attr, src_attr_value) - - # Wire - self.source[0].on_trait_change(self._update, self.source[1]) - - @contextlib.contextmanager - def _busy_updating(self): - self.updating = True - try: - yield - finally: - self.updating = False - - def _update(self, name, old, new): - if self.updating: - return - with self._busy_updating(): - for obj, attr in self.targets: - setattr(obj, attr, new) - - def unlink(self): - self.source[0].on_trait_change(self._update, self.source[1], remove=True) - self.source = None - self.targets = [] - -dlink = directional_link - - -#----------------------------------------------------------------------------- -# Base TraitType for all traits -#----------------------------------------------------------------------------- - - -class TraitType(object): - """A base class for all trait descriptors. - - Notes - ----- - Our implementation of traits is based on Python's descriptor - prototol. This class is the base class for all such descriptors. The - only magic we use is a custom metaclass for the main :class:`HasTraits` - class that does the following: - - 1. Sets the :attr:`name` attribute of every :class:`TraitType` - instance in the class dict to the name of the attribute. - 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 - accept superclasses for :class:`This` values. - """ - - metadata = {} - default_value = Undefined - allow_none = False - info_text = 'any value' - - def __init__(self, default_value=NoDefaultSpecified, allow_none=None, **metadata): - """Create a TraitType. - """ - if default_value is not NoDefaultSpecified: - self.default_value = default_value - if allow_none is not None: - self.allow_none = allow_none - - if 'default' in metadata: - # Warn the user that they probably meant default_value. - warn( - "Parameter 'default' passed to TraitType. " - "Did you mean 'default_value'?" - ) - - 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 - - self.init() - - def init(self): - pass - - def get_default_value(self): - """Create a new instance of the default value.""" - return self.default_value - - def instance_init(self): - """Part of the initialization which may depends on the underlying - HasTraits instance. - - It is typically overloaded for specific trait types. - - This method is called by :meth:`HasTraits.__new__` and in the - :meth:`TraitType.instance_init` method of trait types holding - other trait types. - """ - pass - - def init_default_value(self, obj): - """Instantiate the default value for the trait type. - - This method is called by :meth:`TraitType.set_default_value` in the - case a default value is provided at construction time or later when - accessing the trait value for the first time in - :meth:`HasTraits.__get__`. - """ - value = self.get_default_value() - value = self._validate(obj, value) - obj._trait_values[self.name] = value - return value - - def set_default_value(self, obj): - """Set the default value on a per instance basis. - - This method is called by :meth:`HasTraits.__new__` to instantiate and - validate the default value. The creation and validation of - default values must be delayed until the parent :class:`HasTraits` - class has been instantiated. - Parameters - ---------- - obj : :class:`HasTraits` instance - The parent :class:`HasTraits` instance that has just been - created. - """ - # 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. - self.init_default_value(obj) - return - # Complete the dynamic initialization. - obj._trait_dyn_inits[self.name] = meth_name - - def __get__(self, obj, cls=None): - """Get the value of the trait by self.name for the instance. - - Default values are instantiated when :meth:`HasTraits.__new__` - is called. Thus by the time this method gets called either the - default value or a user defined value (they called :meth:`__set__`) - is in the :class:`HasTraits` instance. - """ - if obj is None: - return self - else: - try: - value = obj._trait_values[self.name] - except KeyError: - # Check for a dynamic initializer. - if self.name in obj._trait_dyn_inits: - method = getattr(obj, obj._trait_dyn_inits[self.name]) - value = method() - # FIXME: Do we really validate here? - value = self._validate(obj, value) - obj._trait_values[self.name] = value - return value - else: - return self.init_default_value(obj) - except Exception: - # HasTraits should call set_default_value to populate - # this. So this should never be reached. - raise TraitError('Unexpected error in TraitType: ' - 'default value not set properly') - else: - return value - - def __set__(self, obj, value): - new_value = self._validate(obj, value) - try: - old_value = obj._trait_values[self.name] - except KeyError: - old_value = Undefined - - obj._trait_values[self.name] = new_value - try: - silent = bool(old_value == new_value) - except: - # if there is an error in comparing, default to notify - silent = False - if silent is not True: - # we explicitly compare silent to True just in case the equality - # comparison above returns something other than True/False - obj._notify_trait(self.name, old_value, new_value) - - def _validate(self, obj, value): - if value is None and self.allow_none: - return value - if hasattr(self, 'validate'): - value = self.validate(obj, value) - if obj._cross_validation_lock is False: - value = self._cross_validate(obj, value) - return value - - def _cross_validate(self, obj, value): - if hasattr(obj, '_%s_validate' % self.name): - cross_validate = getattr(obj, '_%s_validate' % self.name) - value = cross_validate(value, self) - return value - - def __or__(self, other): - if isinstance(other, Union): - return Union([self] + other.trait_types) - else: - return Union([self, other]) - - def info(self): - return self.info_text - - def error(self, obj, value): - if obj is not None: - e = "The '%s' trait 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' trait must be %s, but a value of %r was specified." \ - % (self.name, self.info(), repr_type(value)) - raise TraitError(e) - - def get_metadata(self, key, default=None): - return getattr(self, '_metadata', {}).get(key, default) - - def set_metadata(self, key, value): - getattr(self, '_metadata', {})[key] = value - - -#----------------------------------------------------------------------------- -# The HasTraits implementation -#----------------------------------------------------------------------------- - - -class MetaHasTraits(type): - """A metaclass for HasTraits. - - This metaclass makes sure that any TraitType class attributes are - instantiated and sets their name attribute. - """ - - def __new__(mcls, name, bases, classdict): - """Create the HasTraits class. - - This instantiates all TraitTypes in the class dict and sets their - :attr:`name` attribute. - """ - # print "MetaHasTraitlets (mcls, name): ", mcls, name - # print "MetaHasTraitlets (bases): ", bases - # print "MetaHasTraitlets (classdict): ", classdict - for k,v in iteritems(classdict): - if isinstance(v, TraitType): - v.name = k - elif inspect.isclass(v): - if issubclass(v, TraitType): - vinst = v() - vinst.name = k - classdict[k] = vinst - return super(MetaHasTraits, mcls).__new__(mcls, name, bases, classdict) - - def __init__(cls, name, bases, classdict): - """Finish initializing the HasTraits class. - - This sets the :attr:`this_class` attribute of each TraitType in the - class dict to the newly created class ``cls``. - """ - for k, v in iteritems(classdict): - if isinstance(v, TraitType): - v.this_class = cls - super(MetaHasTraits, cls).__init__(name, bases, classdict) - - -class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)): - - def __new__(cls, *args, **kw): - # This is needed because object.__new__ only accepts - # the cls argument. - new_meth = super(HasTraits, cls).__new__ - if new_meth is object.__new__: - inst = new_meth(cls) - else: - inst = new_meth(cls, **kw) - inst._trait_values = {} - inst._trait_notifiers = {} - inst._trait_dyn_inits = {} - inst._cross_validation_lock = True - # Here we tell all the TraitType instances to set their default - # values on the instance. - for key in dir(cls): - # 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: - if isinstance(value, TraitType): - value.instance_init() - if key not in kw: - value.set_default_value(inst) - inst._cross_validation_lock = False - return inst - - def __init__(self, *args, **kw): - # Allow trait values to be set using keyword arguments. - # We need to use setattr for this to trigger validation and - # notifications. - with self.hold_trait_notifications(): - for key, value in iteritems(kw): - setattr(self, key, value) - - @contextlib.contextmanager - def hold_trait_notifications(self): - """Context manager for bundling trait change notifications and cross - validation. - - Use this when doing multiple trait assignments (init, config), to avoid - race conditions in trait notifiers requesting other trait values. - All trait notifications will fire after all values have been assigned. - """ - if self._cross_validation_lock is True: - yield - return - else: - self._cross_validation_lock = True - cache = {} - notifications = {} - _notify_trait = self._notify_trait - - def cache_values(*a): - cache[a[0]] = a - - def hold_notifications(*a): - notifications[a[0]] = a - - self._notify_trait = cache_values - - try: - yield - finally: - try: - self._notify_trait = hold_notifications - for name in cache: - if hasattr(self, '_%s_validate' % name): - cross_validate = getattr(self, '_%s_validate' % name) - setattr(self, name, cross_validate(getattr(self, name), self)) - except TraitError as e: - self._notify_trait = lambda *x: None - for name in cache: - if cache[name][1] is not Undefined: - setattr(self, name, cache[name][1]) - else: - delattr(self, name) - cache = {} - notifications = {} - raise e - finally: - self._notify_trait = _notify_trait - self._cross_validation_lock = False - if isinstance(_notify_trait, types.MethodType): - # FIXME: remove when support is bumped to 3.4. - # when original method is restored, - # remove the redundant value from __dict__ - # (only used to preserve pickleability on Python < 3.4) - self.__dict__.pop('_notify_trait', None) - # trigger delayed notifications - for v in dict(cache, **notifications).values(): - self._notify_trait(*v) - - def _notify_trait(self, name, old_value, new_value): - - # First dynamic ones - callables = [] - callables.extend(self._trait_notifiers.get(name,[])) - callables.extend(self._trait_notifiers.get('anytrait',[])) - - # 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 = 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 TraitError('a trait changed callback ' - 'must have 0-3 arguments.') - else: - raise TraitError('a trait changed callback ' - 'must be callable.') - - - def _add_notifiers(self, handler, name): - if name not in self._trait_notifiers: - nlist = [] - self._trait_notifiers[name] = nlist - else: - nlist = self._trait_notifiers[name] - if handler not in nlist: - nlist.append(handler) - - def _remove_notifiers(self, handler, name): - if name in self._trait_notifiers: - nlist = self._trait_notifiers[name] - try: - index = nlist.index(handler) - except ValueError: - pass - else: - del nlist[index] - - def on_trait_change(self, handler, name=None, remove=False): - """Setup a handler to be called when a trait changes. - - This is used to setup dynamic notifications of trait changes. - - Static handlers can be created by creating methods on a HasTraits - subclass with the naming convention '_[traitname]_changed'. Thus, - to create static handler for the trait '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 trait 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 traits. 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) - - @classmethod - def class_trait_names(cls, **metadata): - """Get a list of all the names of this class' traits. - - This method is just like the :meth:`trait_names` method, - but is unbound. - """ - return cls.class_traits(**metadata).keys() - - @classmethod - def class_traits(cls, **metadata): - """Get a `dict` of all the traits of this class. The dictionary - is keyed on the name and the values are the TraitType objects. - - This method is just like the :meth:`traits` method, but is unbound. - - The TraitTypes returned don't know anything about the values - that the various HasTrait's instances are holding. - - The metadata kwargs allow functions to be passed in which - filter traits based on metadata values. The functions should - take a single value as an argument and return a boolean. If - any function returns False, then the trait is not included in - the output. This does not allow for any simple way of - testing that a metadata name exists and has any - value because get_metadata returns None if a metadata key - doesn't exist. - """ - traits = dict([memb for memb in getmembers(cls) if - isinstance(memb[1], TraitType)]) - - if len(metadata) == 0: - return traits - - for meta_name, meta_eval in metadata.items(): - if type(meta_eval) is not FunctionType: - metadata[meta_name] = _SimpleTest(meta_eval) - - result = {} - for name, trait in traits.items(): - for meta_name, meta_eval in metadata.items(): - if not meta_eval(trait.get_metadata(meta_name)): - break - else: - result[name] = trait - - return result - - def trait_names(self, **metadata): - """Get a list of all the names of this class' traits.""" - return self.traits(**metadata).keys() - - def traits(self, **metadata): - """Get a `dict` of all the traits of this class. The dictionary - is keyed on the name and the values are the TraitType objects. - - The TraitTypes returned don't know anything about the values - that the various HasTrait's instances are holding. - - The metadata kwargs allow functions to be passed in which - filter traits based on metadata values. The functions should - take a single value as an argument and return a boolean. If - any function returns False, then the trait is not included in - the output. This does not allow for any simple way of - testing that a metadata name exists and has any - value because get_metadata returns None if a metadata key - doesn't exist. - """ - traits = dict([memb for memb in getmembers(self.__class__) if - isinstance(memb[1], TraitType)]) - - if len(metadata) == 0: - return traits - - for meta_name, meta_eval in metadata.items(): - if type(meta_eval) is not FunctionType: - metadata[meta_name] = _SimpleTest(meta_eval) - - result = {} - for name, trait in traits.items(): - for meta_name, meta_eval in metadata.items(): - if not meta_eval(trait.get_metadata(meta_name)): - break - else: - result[name] = trait - - return result - - def trait_metadata(self, traitname, key, default=None): - """Get metadata values for trait by key.""" - try: - trait = getattr(self.__class__, traitname) - except AttributeError: - raise TraitError("Class %s does not have a trait named %s" % - (self.__class__.__name__, traitname)) - else: - return trait.get_metadata(key, default) - - def add_trait(self, traitname, trait): - """Dynamically add a trait attribute to the HasTraits instance.""" - self.__class__ = type(self.__class__.__name__, (self.__class__,), - {traitname: trait}) - trait.set_default_value(self) - -#----------------------------------------------------------------------------- -# Actual TraitTypes implementations/subclasses -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# TraitTypes subclasses for handling classes and instances of classes -#----------------------------------------------------------------------------- - - -class ClassBasedTraitType(TraitType): - """ - A trait with error reporting and string -> type resolution for Type, - Instance and This. - """ - - def _resolve_string(self, string): - """ - Resolve a string supplied for a type into an actual object. - """ - return import_item(string) - - def error(self, obj, value): - kind = type(value) - if (not py3compat.PY3) and kind is InstanceType: - msg = 'class %s' % value.__class__.__name__ - else: - msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) ) - - if obj is not None: - e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \ - % (self.name, class_of(obj), - self.info(), msg) - else: - e = "The '%s' trait must be %s, but a value of %r was specified." \ - % (self.name, self.info(), msg) - - raise TraitError(e) - - -class Type(ClassBasedTraitType): - """A trait whose value must be a subclass of a specified class.""" - - def __init__ (self, default_value=None, klass=None, allow_none=False, - **metadata): - """Construct a Type trait - - A Type trait specifies that its values must be subclasses of - a particular class. - - If only ``default_value`` is given, it is used for the ``klass`` as - well. - - Parameters - ---------- - 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 - :class:`HasTraits` class is instantiated. - klass : class, str, None - Values of this trait must be a subclass of klass. The klass - may be specified in a string like: 'foo.bar.MyClass'. - The string is resolved into real class, when the parent - :class:`HasTraits` class is instantiated. - allow_none : bool [ default True ] - 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 - - if not (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)): - raise TraitError("A Type trait must specify a class.") - - self.klass = klass - - super(Type, self).__init__(default_value, allow_none=allow_none, **metadata) - - def validate(self, obj, value): - """Validates that the value is a valid object instance.""" - if isinstance(value, py3compat.string_types): - try: - value = self._resolve_string(value) - except ImportError: - raise TraitError("The '%s' trait of %s instance must be a type, but " - "%r could not be imported" % (self.name, obj, value)) - try: - if issubclass(value, self.klass): - return value - except: - pass - - self.error(obj, value) - - def info(self): - """ Returns a description of the trait.""" - if isinstance(self.klass, py3compat.string_types): - klass = self.klass - else: - klass = self.klass.__name__ - result = 'a subclass of ' + klass - if self.allow_none: - return result + ' or None' - return result - - def instance_init(self): - self._resolve_classes() - super(Type, self).instance_init() - - def _resolve_classes(self): - if isinstance(self.klass, py3compat.string_types): - self.klass = self._resolve_string(self.klass) - if isinstance(self.default_value, py3compat.string_types): - self.default_value = self._resolve_string(self.default_value) - - def get_default_value(self): - return self.default_value - - -class DefaultValueGenerator(object): - """A class for generating new default value instances.""" - - def __init__(self, *args, **kw): - self.args = args - self.kw = kw - - def generate(self, klass): - return klass(*self.args, **self.kw) - - -class Instance(ClassBasedTraitType): - """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. - - Subclasses can declare default classes by overriding the klass attribute - """ - - klass = None - - def __init__(self, klass=None, args=None, kw=None, allow_none=False, - **metadata ): - """Construct an Instance trait. - - This trait allows values that are instances of a particular - class or its subclasses. 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. - - Parameters - ---------- - klass : class, str - The class that forms the basis for the trait. Class names - can also be specified as strings, like 'foo.bar.Bar'. - args : tuple - Positional arguments for generating the default value. - kw : dict - Keyword arguments for generating the default value. - allow_none : bool [default True] - Indicates whether None is allowed as a value. - - Notes - ----- - 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 exactly one of ``args`` or ``kw`` is - None, the None is replaced by ``()`` or ``{}``, respectively. - """ - if klass is None: - klass = self.klass - - if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)): - self.klass = klass - else: - raise TraitError('The klass attribute must be a class' - ' not: %r' % klass) - - # self.klass is a class, so handle default_value - if args is None and kw is None: - default_value = None - else: - if args is None: - # kw is not None - args = () - elif kw is None: - # args is not None - kw = {} - - if not isinstance(kw, dict): - raise TraitError("The 'kw' argument must be a dict or None.") - if not isinstance(args, tuple): - raise TraitError("The 'args' argument must be a tuple or None.") - - default_value = DefaultValueGenerator(*args, **kw) - - super(Instance, self).__init__(default_value, allow_none=allow_none, **metadata) - - def validate(self, obj, value): - if isinstance(value, self.klass): - return value - else: - self.error(obj, value) - - def info(self): - if isinstance(self.klass, py3compat.string_types): - klass = self.klass - else: - klass = self.klass.__name__ - result = class_of(klass) - if self.allow_none: - return result + ' or None' - - return result - - def instance_init(self): - self._resolve_classes() - super(Instance, self).instance_init() - - def _resolve_classes(self): - if isinstance(self.klass, py3compat.string_types): - self.klass = self._resolve_string(self.klass) - - def get_default_value(self): - """Instantiate a default value instance. - - This is called when the containing HasTraits classes' - :meth:`__new__` method is called to ensure that a unique instance - is created for each HasTraits instance. - """ - dv = self.default_value - if isinstance(dv, DefaultValueGenerator): - return dv.generate(self.klass) - else: - return dv - - -class ForwardDeclaredMixin(object): - """ - Mixin for forward-declared versions of Instance and Type. - """ - def _resolve_string(self, string): - """ - Find the specified class name by looking for it in the module in which - our this_class attribute was defined. - """ - modname = self.this_class.__module__ - return import_item('.'.join([modname, string])) - - -class ForwardDeclaredType(ForwardDeclaredMixin, Type): - """ - Forward-declared version of Type. - """ - pass - - -class ForwardDeclaredInstance(ForwardDeclaredMixin, Instance): - """ - Forward-declared version of Instance. - """ - pass - - -class This(ClassBasedTraitType): - """A trait for instances of the class containing this trait. - - Because how how and when class bodies are executed, the ``This`` - trait can only have a default value of None. This, and because we - always validate default values, ``allow_none`` is *always* true. - """ - - info_text = 'an instance of the same type as the receiver or None' - - def __init__(self, **metadata): - super(This, self).__init__(None, **metadata) - - def validate(self, obj, value): - # What if value is a superclass of obj.__class__? This is - # complicated if it was the superclass that defined the This - # trait. - if isinstance(value, self.this_class) or (value is None): - return value - else: - self.error(obj, value) - - -class Union(TraitType): - """A trait type representing a Union type.""" - - def __init__(self, trait_types, **metadata): - """Construct a Union trait. - - This trait allows values that are allowed by at least one of the - specified trait types. A Union traitlet cannot have metadata on - its own, besides the metadata of the listed types. - - Parameters - ---------- - trait_types: sequence - The list of trait types of length at least 1. - - Notes - ----- - Union([Float(), Bool(), Int()]) attempts to validate the provided values - with the validation function of Float, then Bool, and finally Int. - """ - self.trait_types = trait_types - self.info_text = " or ".join([tt.info_text for tt in self.trait_types]) - self.default_value = self.trait_types[0].get_default_value() - super(Union, self).__init__(**metadata) - - def instance_init(self): - for trait_type in self.trait_types: - trait_type.name = self.name - trait_type.this_class = self.this_class - trait_type.instance_init() - super(Union, self).instance_init() - - def validate(self, obj, value): - for trait_type in self.trait_types: - try: - v = trait_type._validate(obj, value) - self._metadata = trait_type._metadata - return v - except TraitError: - continue - self.error(obj, value) - - def __or__(self, other): - if isinstance(other, Union): - return Union(self.trait_types + other.trait_types) - else: - return Union(self.trait_types + [other]) - -#----------------------------------------------------------------------------- -# Basic TraitTypes implementations/subclasses -#----------------------------------------------------------------------------- - - -class Any(TraitType): - default_value = None - info_text = 'any value' - - -class Int(TraitType): - """An int trait.""" - - default_value = 0 - info_text = 'an int' - - def validate(self, obj, value): - if isinstance(value, int): - return value - self.error(obj, value) - -class CInt(Int): - """A casting version of the int trait.""" - - def validate(self, obj, value): - try: - return int(value) - except: - self.error(obj, value) - -if py3compat.PY3: - Long, CLong = Int, CInt - Integer = Int -else: - class Long(TraitType): - """A long integer trait.""" - - default_value = 0 - 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 CLong(Long): - """A casting version of the long integer trait.""" - - def validate(self, obj, value): - try: - return long(value) - except: - self.error(obj, value) - - class Integer(TraitType): - """An integer trait. - - Longs that are unnecessary (<= sys.maxint) are cast to ints.""" - - default_value = 0 - info_text = 'an integer' - - def validate(self, obj, value): - if isinstance(value, int): - return value - if isinstance(value, long): - # downcast longs that fit in int: - # note that int(n > sys.maxint) returns a long, so - # we don't need a condition on this cast - return int(value) - if sys.platform == "cli": - from System import Int64 - if isinstance(value, Int64): - return int(value) - self.error(obj, value) - - -class Float(TraitType): - """A float trait.""" - - 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 CFloat(Float): - """A casting version of the float trait.""" - - def validate(self, obj, value): - try: - return float(value) - except: - self.error(obj, value) - -class Complex(TraitType): - """A trait for complex numbers.""" - - 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 CComplex(Complex): - """A casting version of the complex number trait.""" - - def validate (self, obj, value): - try: - return complex(value) - except: - self.error(obj, value) - -# We should always be explicit about whether we're using bytes or unicode, both -# for Python 3 conversion and for reliable unicode behaviour on Python 2. So -# we don't have a Str type. -class Bytes(TraitType): - """A trait for byte strings.""" - - default_value = b'' - info_text = 'a bytes object' - - def validate(self, obj, value): - if isinstance(value, bytes): - return value - self.error(obj, value) - - -class CBytes(Bytes): - """A casting version of the byte string trait.""" - - def validate(self, obj, value): - try: - return bytes(value) - except: - self.error(obj, value) - - -class Unicode(TraitType): - """A trait for unicode strings.""" - - default_value = u'' - info_text = 'a unicode string' - - def validate(self, obj, value): - if isinstance(value, py3compat.unicode_type): - return value - if isinstance(value, bytes): - try: - return value.decode('ascii', 'strict') - except UnicodeDecodeError: - msg = "Could not decode {!r} for unicode trait '{}' of {} instance." - raise TraitError(msg.format(value, self.name, class_of(obj))) - self.error(obj, value) - - -class CUnicode(Unicode): - """A casting version of the unicode trait.""" - - def validate(self, obj, value): - try: - return py3compat.unicode_type(value) - except: - self.error(obj, value) - - -class ObjectName(TraitType): - """A string holding a valid object name in this version of Python. - - This does not check that the name exists in any scope.""" - info_text = "a valid object identifier in Python" - - if py3compat.PY3: - # Python 3: - coerce_str = staticmethod(lambda _,s: s) - - else: - # Python 2: - def coerce_str(self, obj, value): - "In Python 2, coerce ascii-only unicode to str" - if isinstance(value, unicode): - try: - return str(value) - except UnicodeEncodeError: - self.error(obj, value) - return value - - def validate(self, obj, value): - value = self.coerce_str(obj, value) - - if isinstance(value, string_types) and py3compat.isidentifier(value): - return value - self.error(obj, value) - -class DottedObjectName(ObjectName): - """A string holding a valid dotted object name in Python, such as A.b3._c""" - def validate(self, obj, value): - value = self.coerce_str(obj, value) - - if isinstance(value, string_types) and py3compat.isidentifier(value, dotted=True): - return value - self.error(obj, value) - - -class Bool(TraitType): - """A boolean (True, False) trait.""" - - default_value = False - info_text = 'a boolean' - - def validate(self, obj, value): - if isinstance(value, bool): - return value - self.error(obj, value) - - -class CBool(Bool): - """A casting version of the boolean trait.""" - - def validate(self, obj, value): - try: - return bool(value) - except: - self.error(obj, value) - - -class Enum(TraitType): - """An enum that whose value must be in a given sequence.""" - - def __init__(self, values, default_value=None, **metadata): - self.values = values - super(Enum, self).__init__(default_value, **metadata) - - def validate(self, obj, 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): - """An enum of strings that are caseless in validate.""" - - def validate(self, obj, value): - if not isinstance(value, py3compat.string_types): - self.error(obj, value) - - for v in self.values: - if v.lower() == value.lower(): - return v - self.error(obj, value) - -class Container(Instance): - """An instance of a container (list, set, etc.) - - To be subclassed by overriding klass. - """ - klass = None - _cast_types = () - _valid_defaults = SequenceTypes - _trait = None - - def __init__(self, trait=None, default_value=None, allow_none=False, - **metadata): - """Create a container trait type from a list, set, or tuple. - - The default value is created by doing ``List(default_value)``, - which creates a copy of the ``default_value``. - - ``trait`` can be specified, which restricts the type of elements - in the container to that TraitType. - - If only one arg is given and it is not a Trait, it is taken as - ``default_value``: - - ``c = List([1,2,3])`` - - Parameters - ---------- - - trait : TraitType [ optional ] - the type for restricting the contents of the Container. If unspecified, - types are not checked. - - default_value : SequenceType [ optional ] - The default value for the Trait. Must be list/tuple/set, and - will be cast to the container type. - - allow_none : bool [ default False ] - Whether to allow the value to be None - - **metadata : any - further keys for extensions to the Trait (e.g. config) - - """ - # allow List([values]): - if default_value is None and not is_trait(trait): - default_value = trait - trait = None - - if default_value is None: - args = () - elif isinstance(default_value, self._valid_defaults): - args = (default_value,) - else: - raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value)) - - if is_trait(trait): - self._trait = trait() if isinstance(trait, type) else trait - self._trait.name = 'element' - elif trait is not None: - raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait)) - - super(Container,self).__init__(klass=self.klass, args=args, - allow_none=allow_none, **metadata) - - def element_error(self, obj, element, validator): - e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \ - % (self.name, class_of(obj), validator.info(), repr_type(element)) - raise TraitError(e) - - def validate(self, obj, value): - if isinstance(value, self._cast_types): - value = self.klass(value) - value = super(Container, self).validate(obj, value) - if value is None: - return value - - value = self.validate_elements(obj, value) - - return value - - def validate_elements(self, obj, value): - validated = [] - if self._trait is None or isinstance(self._trait, Any): - return value - for v in value: - try: - v = self._trait._validate(obj, v) - except TraitError: - self.element_error(obj, v, self._trait) - else: - validated.append(v) - return self.klass(validated) - - def instance_init(self): - if isinstance(self._trait, TraitType): - self._trait.this_class = self.this_class - self._trait.instance_init() - super(Container, self).instance_init() - - -class List(Container): - """An instance of a Python list.""" - klass = list - _cast_types = (tuple,) - - def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, **metadata): - """Create a List trait type from a list, set, or tuple. - - The default value is created by doing ``List(default_value)``, - which creates a copy of the ``default_value``. - - ``trait`` can be specified, which restricts the type of elements - in the container to that TraitType. - - If only one arg is given and it is not a Trait, it is taken as - ``default_value``: - - ``c = List([1,2,3])`` - - Parameters - ---------- - - trait : TraitType [ optional ] - the type for restricting the contents of the Container. If unspecified, - types are not checked. - - default_value : SequenceType [ optional ] - The default value for the Trait. Must be list/tuple/set, and - will be cast to the container type. - - minlen : Int [ default 0 ] - The minimum length of the input list - - maxlen : Int [ default sys.maxsize ] - The maximum length of the input list - - allow_none : bool [ default False ] - Whether to allow the value to be None - - **metadata : any - further keys for extensions to the Trait (e.g. config) - - """ - self._minlen = minlen - self._maxlen = maxlen - super(List, self).__init__(trait=trait, default_value=default_value, - **metadata) - - def length_error(self, obj, value): - e = "The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified." \ - % (self.name, class_of(obj), self._minlen, self._maxlen, value) - raise TraitError(e) - - def validate_elements(self, obj, value): - length = len(value) - if length < self._minlen or length > self._maxlen: - self.length_error(obj, value) - - return super(List, self).validate_elements(obj, value) - - def validate(self, obj, value): - value = super(List, self).validate(obj, value) - value = self.validate_elements(obj, value) - return value - - -class Set(List): - """An instance of a Python set.""" - klass = set - _cast_types = (tuple, list) - - -class Tuple(Container): - """An instance of a Python tuple.""" - klass = tuple - _cast_types = (list,) - - def __init__(self, *traits, **metadata): - """Tuple(*traits, default_value=None, **medatata) - - Create a tuple from a list, set, or tuple. - - Create a fixed-type tuple with Traits: - - ``t = Tuple(Int, Str, CStr)`` - - would be length 3, with Int,Str,CStr for each element. - - If only one arg is given and it is not a Trait, it is taken as - default_value: - - ``t = Tuple((1,2,3))`` - - Otherwise, ``default_value`` *must* be specified by keyword. - - Parameters - ---------- - - *traits : TraitTypes [ optional ] - the types for restricting the contents of the Tuple. If unspecified, - types are not checked. If specified, then each positional argument - corresponds to an element of the tuple. Tuples defined with traits - are of fixed length. - - default_value : SequenceType [ optional ] - The default value for the Tuple. Must be list/tuple/set, and - will be cast to a tuple. If `traits` are specified, the - `default_value` must conform to the shape and type they specify. - - allow_none : bool [ default False ] - Whether to allow the value to be None - - **metadata : any - further keys for extensions to the Trait (e.g. config) - - """ - default_value = metadata.pop('default_value', None) - allow_none = metadata.pop('allow_none', True) - - # allow Tuple((values,)): - if len(traits) == 1 and default_value is None and not is_trait(traits[0]): - default_value = traits[0] - traits = () - - if default_value is None: - args = () - elif isinstance(default_value, self._valid_defaults): - args = (default_value,) - else: - raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value)) - - self._traits = [] - for trait in traits: - t = trait() if isinstance(trait, type) else trait - t.name = 'element' - self._traits.append(t) - - if self._traits and default_value is None: - # don't allow default to be an empty container if length is specified - args = None - super(Container,self).__init__(klass=self.klass, args=args, allow_none=allow_none, **metadata) - - def validate_elements(self, obj, value): - if not self._traits: - # nothing to validate - return value - if len(value) != len(self._traits): - e = "The '%s' trait of %s instance requires %i elements, but a value of %s was specified." \ - % (self.name, class_of(obj), len(self._traits), repr_type(value)) - raise TraitError(e) - - validated = [] - for t, v in zip(self._traits, value): - try: - v = t._validate(obj, v) - except TraitError: - self.element_error(obj, v, t) - else: - validated.append(v) - return tuple(validated) - - def instance_init(self): - for trait in self._traits: - if isinstance(trait, TraitType): - trait.this_class = self.this_class - trait.instance_init() - super(Container, self).instance_init() - - -class Dict(Instance): - """An instance of a Python dict.""" - _trait = None - - def __init__(self, trait=None, default_value=NoDefaultSpecified, allow_none=False, **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``. - - trait : TraitType [ optional ] - the type for restricting the contents of the Container. If unspecified, - types are not checked. - - default_value : SequenceType [ optional ] - The default value for the Dict. Must be dict, tuple, or None, and - will be cast to a dict if not None. If `trait` is specified, the - `default_value` must conform to the constraints it specifies. - - allow_none : bool [ default False ] - Whether to allow the value to be None - - """ - if default_value is NoDefaultSpecified and trait is not None: - if not is_trait(trait): - default_value = trait - trait = None - if default_value is NoDefaultSpecified: - default_value = {} - if default_value is None: - args = None - 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) - - if is_trait(trait): - self._trait = trait() if isinstance(trait, type) else trait - self._trait.name = 'element' - elif trait is not None: - raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait)) - - super(Dict,self).__init__(klass=dict, args=args, - allow_none=allow_none, **metadata) - - def element_error(self, obj, element, validator): - e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \ - % (self.name, class_of(obj), validator.info(), repr_type(element)) - raise TraitError(e) - - def validate(self, obj, value): - value = super(Dict, self).validate(obj, value) - if value is None: - return value - value = self.validate_elements(obj, value) - return value - - def validate_elements(self, obj, value): - if self._trait is None or isinstance(self._trait, Any): - return value - validated = {} - for key in value: - v = value[key] - try: - v = self._trait._validate(obj, v) - except TraitError: - self.element_error(obj, v, self._trait) - else: - validated[key] = v - return self.klass(validated) - - def instance_init(self): - if isinstance(self._trait, TraitType): - self._trait.this_class = self.this_class - self._trait.instance_init() - super(Dict, self).instance_init() - - -class EventfulDict(Instance): - """An instance of an EventfulDict.""" - - def __init__(self, default_value={}, allow_none=False, **metadata): - """Create a EventfulDict trait type from a dict. - - The default value is created by doing - ``eventful.EvenfulDict(default_value)``, which creates a copy of the - ``default_value``. - """ - if default_value is None: - args = None - elif isinstance(default_value, dict): - args = (default_value,) - elif isinstance(default_value, SequenceTypes): - args = (default_value,) - else: - raise TypeError('default value of EventfulDict was %s' % default_value) - - super(EventfulDict, self).__init__(klass=eventful.EventfulDict, args=args, - allow_none=allow_none, **metadata) - - -class EventfulList(Instance): - """An instance of an EventfulList.""" - - def __init__(self, default_value=None, allow_none=False, **metadata): - """Create a EventfulList trait type from a dict. - - The default value is created by doing - ``eventful.EvenfulList(default_value)``, which creates a copy of the - ``default_value``. - """ - if default_value is None: - args = ((),) - else: - args = (default_value,) - - super(EventfulList, self).__init__(klass=eventful.EventfulList, args=args, - allow_none=allow_none, **metadata) - - -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], py3compat.string_types) and isinstance(value[1], int): - port = value[1] - if port >= 0 and port <= 65535: - return value - self.error(obj, value) - -class CRegExp(TraitType): - """A casting compiled regular expression trait. - - Accepts both strings and compiled regular expressions. The resulting - attribute will be a compiled regular expression.""" - - info_text = 'a regular expression' - - def validate(self, obj, value): - try: - return re.compile(value) - except: - self.error(obj, value) +from traitlets import * diff --git a/traitlets/__init__.py b/traitlets/__init__.py new file mode 100644 index 0000000..ad54a5b --- /dev/null +++ b/traitlets/__init__.py @@ -0,0 +1,5 @@ +# FIXME: import IPython first, to avoid circular imports +# this shouldn't be needed after finishing the big split +import IPython + +from .traitlets import * diff --git a/IPython/config/__init__.py b/traitlets/config/__init__.py similarity index 100% rename from IPython/config/__init__.py rename to traitlets/config/__init__.py diff --git a/IPython/config/application.py b/traitlets/config/application.py similarity index 98% rename from IPython/config/application.py rename to traitlets/config/application.py index bb05472..d44899e 100644 --- a/IPython/config/application.py +++ b/traitlets/config/application.py @@ -16,12 +16,12 @@ from collections import defaultdict from decorator import decorator -from IPython.config.configurable import SingletonConfigurable -from IPython.config.loader import ( +from traitlets.config.configurable import SingletonConfigurable +from traitlets.config.loader import ( KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, JSONFileConfigLoader ) -from IPython.utils.traitlets import ( +from traitlets.traitlets import ( Unicode, List, Enum, Dict, Instance, TraitError ) from IPython.utils.importstring import import_item @@ -227,7 +227,7 @@ class Application(SingletonConfigurable): # and the second being the help string for the subcommand subcommands = Dict() # parse_command_line will initialize a subapp, if requested - subapp = Instance('IPython.config.application.Application', allow_none=True) + subapp = Instance('traitlets.config.application.Application', allow_none=True) # extra command-line arguments that don't set config values extra_args = List(Unicode) diff --git a/IPython/config/configurable.py b/traitlets/config/configurable.py similarity index 97% rename from IPython/config/configurable.py rename to traitlets/config/configurable.py index d1a8e68..9d79a40 100644 --- a/IPython/config/configurable.py +++ b/traitlets/config/configurable.py @@ -10,7 +10,7 @@ import logging from copy import deepcopy from .loader import Config, LazyConfigValue -from IPython.utils.traitlets import HasTraits, Instance +from traitlets.traitlets import HasTraits, Instance from IPython.utils.text import indent, wrap_paragraphs from IPython.utils.py3compat import iteritems @@ -34,7 +34,7 @@ class MultipleInstanceError(ConfigurableError): class Configurable(HasTraits): config = Instance(Config, (), {}) - parent = Instance('IPython.config.configurable.Configurable', allow_none=True) + parent = Instance('traitlets.config.configurable.Configurable', allow_none=True) def __init__(self, **kwargs): """Create a configurable given a config config. @@ -329,7 +329,7 @@ class SingletonConfigurable(Configurable): Create a singleton class using instance, and retrieve it:: - >>> from IPython.config.configurable import SingletonConfigurable + >>> from traitlets.config.configurable import SingletonConfigurable >>> class Foo(SingletonConfigurable): pass >>> foo = Foo.instance() >>> foo == Foo.instance() diff --git a/IPython/config/loader.py b/traitlets/config/loader.py similarity index 99% rename from IPython/config/loader.py rename to traitlets/config/loader.py index cdd0367..6d203a3 100644 --- a/IPython/config/loader.py +++ b/traitlets/config/loader.py @@ -17,7 +17,7 @@ from IPython.utils.path import filefind, get_ipython_dir from IPython.utils import py3compat from IPython.utils.encoding import DEFAULT_ENCODING from IPython.utils.py3compat import unicode_type, iteritems -from IPython.utils.traitlets import HasTraits, List, Any +from traitlets.traitlets import HasTraits, List, Any #----------------------------------------------------------------------------- # Exceptions @@ -330,7 +330,7 @@ class ConfigLoader(object): """A base class for config loaders. log : instance of :class:`logging.Logger` to use. - By default loger of :meth:`IPython.config.application.Application.instance()` + By default loger of :meth:`traitlets.config.application.Application.instance()` will be used Examples @@ -558,7 +558,7 @@ class KeyValueConfigLoader(CommandLineConfigLoader): Examples -------- - >>> from IPython.config.loader import KeyValueConfigLoader + >>> from traitlets.config.loader import KeyValueConfigLoader >>> cl = KeyValueConfigLoader() >>> d = cl.load_config(["--A.name='brian'","--B.number=0"]) >>> sorted(d.items()) diff --git a/IPython/config/manager.py b/traitlets/config/manager.py similarity index 97% rename from IPython/config/manager.py rename to traitlets/config/manager.py index 9f7f2f5..b6d47f0 100644 --- a/IPython/config/manager.py +++ b/traitlets/config/manager.py @@ -7,9 +7,9 @@ import io import json import os -from IPython.config import LoggingConfigurable +from traitlets.config import LoggingConfigurable from IPython.utils.py3compat import PY3 -from IPython.utils.traitlets import Unicode +from traitlets.traitlets import Unicode def recursive_update(target, new): diff --git a/IPython/config/tests/__init__.py b/traitlets/config/tests/__init__.py similarity index 100% rename from IPython/config/tests/__init__.py rename to traitlets/config/tests/__init__.py diff --git a/IPython/config/tests/test_application.py b/traitlets/config/tests/test_application.py similarity index 96% rename from IPython/config/tests/test_application.py rename to traitlets/config/tests/test_application.py index 5da6a13..515ba95 100644 --- a/IPython/config/tests/test_application.py +++ b/traitlets/config/tests/test_application.py @@ -1,6 +1,6 @@ # coding: utf-8 """ -Tests for IPython.config.application.Application +Tests for traitlets.config.application.Application """ # Copyright (c) IPython Development Team. @@ -15,15 +15,15 @@ pjoin = os.path.join import nose.tools as nt -from IPython.config.configurable import Configurable -from IPython.config.loader import Config +from traitlets.config.configurable import Configurable +from traitlets.config.loader import Config -from IPython.config.application import ( +from traitlets.config.application import ( Application ) from IPython.utils.tempdir import TemporaryDirectory -from IPython.utils.traitlets import ( +from traitlets.traitlets import ( Bool, Unicode, Integer, List, Dict ) diff --git a/IPython/config/tests/test_configurable.py b/traitlets/config/tests/test_configurable.py similarity index 97% rename from IPython/config/tests/test_configurable.py rename to traitlets/config/tests/test_configurable.py index 29dafac..a6f659a 100644 --- a/IPython/config/tests/test_configurable.py +++ b/traitlets/config/tests/test_configurable.py @@ -1,21 +1,21 @@ # encoding: utf-8 -"""Tests for IPython.config.configurable""" +"""Tests for traitlets.config.configurable""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. from unittest import TestCase -from IPython.config.configurable import ( +from traitlets.config.configurable import ( Configurable, SingletonConfigurable ) -from IPython.utils.traitlets import ( +from traitlets.traitlets import ( Integer, Float, Unicode, List, Dict, Set, ) -from IPython.config.loader import Config +from traitlets.config.loader import Config from IPython.utils.py3compat import PY3 diff --git a/IPython/config/tests/test_loader.py b/traitlets/config/tests/test_loader.py similarity index 97% rename from IPython/config/tests/test_loader.py rename to traitlets/config/tests/test_loader.py index 0cb575e..5c79ec1 100644 --- a/IPython/config/tests/test_loader.py +++ b/traitlets/config/tests/test_loader.py @@ -1,5 +1,5 @@ # encoding: utf-8 -"""Tests for IPython.config.loader""" +"""Tests for traitlets.config.loader""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. @@ -18,7 +18,7 @@ import nose.tools as nt -from IPython.config.loader import ( +from traitlets.config.loader import ( Config, LazyConfigValue, PyFileConfigLoader, diff --git a/traitlets/tests/__init__.py b/traitlets/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/traitlets/tests/__init__.py diff --git a/IPython/utils/tests/test_traitlets.py b/traitlets/tests/test_traitlets.py similarity index 100% rename from IPython/utils/tests/test_traitlets.py rename to traitlets/tests/test_traitlets.py diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py new file mode 100644 index 0000000..bbc4388 --- /dev/null +++ b/traitlets/traitlets.py @@ -0,0 +1,1874 @@ +# 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. Most importantly, we don't provide container + traits (list, dict, tuple) that can trigger notifications if their + contents change. +* API compatibility with enthought.traits + +There are also some important difference in our design: + +* enthought.traits does not validate default values. We do. + +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. + +Inheritance diagram: + +.. inheritance-diagram:: IPython.utils.traitlets + :parts: 3 +""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. +# +# Adapted from enthought.traits, Copyright (c) Enthought, Inc., +# also under the terms of the Modified BSD License. + +import contextlib +import inspect +import re +import sys +import types +from types import FunctionType +try: + from types import ClassType, InstanceType + ClassTypes = (ClassType, type) +except: + ClassTypes = (type,) +from warnings import warn + +from IPython.utils import py3compat +from IPython.utils import eventful +from IPython.utils.getargspec import getargspec +from IPython.utils.importstring import import_item +from IPython.utils.py3compat import iteritems, string_types +from IPython.testing.skipdoctest import skip_doctest + +SequenceTypes = (list, tuple, set, frozenset) + +#----------------------------------------------------------------------------- +# Basic classes +#----------------------------------------------------------------------------- + + +class NoDefaultSpecified ( object ): pass +NoDefaultSpecified = NoDefaultSpecified() + + +class Undefined ( object ): pass +Undefined = Undefined() + +class TraitError(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, py3compat.string_types ): + 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 (not py3compat.PY3) and the_type is InstanceType: + # Old-style class. + the_type = obj.__class__ + msg = '%r %r' % (obj, the_type) + return msg + + +def is_trait(t): + """ Returns whether the given value is an instance or subclass of TraitType. + """ + return (isinstance(t, TraitType) or + (isinstance(t, type) and issubclass(t, TraitType))) + + +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) + ['anytrait'] + """ + if isinstance(name, string_types): + return [name] + elif name is None: + return ['anytrait'] + elif isinstance(name, (list, tuple)): + for n in name: + assert isinstance(n, string_types), "names must be strings" + return name + + +class _SimpleTest: + def __init__ ( self, value ): self.value = value + def __call__ ( self, test ): + return test == self.value + def __repr__(self): + return ">> c = link((obj1, 'value'), (obj2, 'value'), (obj3, 'value')) + >>> obj1.value = 5 # updates other objects as well + """ + updating = False + def __init__(self, *args): + if len(args) < 2: + raise TypeError('At least two traitlets must be provided.') + _validate_link(*args) + + self.objects = {} + + initial = getattr(args[0][0], args[0][1]) + for obj, attr in args: + setattr(obj, attr, initial) + + callback = self._make_closure(obj, attr) + obj.on_trait_change(callback, attr) + self.objects[(obj, attr)] = callback + + @contextlib.contextmanager + def _busy_updating(self): + self.updating = True + try: + yield + finally: + self.updating = False + + def _make_closure(self, sending_obj, sending_attr): + def update(name, old, new): + self._update(sending_obj, sending_attr, new) + return update + + def _update(self, sending_obj, sending_attr, new): + if self.updating: + return + with self._busy_updating(): + for obj, attr in self.objects.keys(): + setattr(obj, attr, new) + + def unlink(self): + for key, callback in self.objects.items(): + (obj, attr) = key + obj.on_trait_change(callback, attr, remove=True) + +@skip_doctest +class directional_link(object): + """Link the trait of a source object with traits of target objects. + + Parameters + ---------- + source : pair of object, name + targets : pairs of objects/attributes + + Examples + -------- + + >>> c = directional_link((src, 'value'), (tgt1, 'value'), (tgt2, 'value')) + >>> src.value = 5 # updates target objects + >>> tgt1.value = 6 # does not update other objects + """ + updating = False + + def __init__(self, source, *targets): + if len(targets) < 1: + raise TypeError('At least two traitlets must be provided.') + _validate_link(source, *targets) + self.source = source + self.targets = targets + + # Update current value + src_attr_value = getattr(source[0], source[1]) + for obj, attr in targets: + setattr(obj, attr, src_attr_value) + + # Wire + self.source[0].on_trait_change(self._update, self.source[1]) + + @contextlib.contextmanager + def _busy_updating(self): + self.updating = True + try: + yield + finally: + self.updating = False + + def _update(self, name, old, new): + if self.updating: + return + with self._busy_updating(): + for obj, attr in self.targets: + setattr(obj, attr, new) + + def unlink(self): + self.source[0].on_trait_change(self._update, self.source[1], remove=True) + self.source = None + self.targets = [] + +dlink = directional_link + + +#----------------------------------------------------------------------------- +# Base TraitType for all traits +#----------------------------------------------------------------------------- + + +class TraitType(object): + """A base class for all trait descriptors. + + Notes + ----- + Our implementation of traits is based on Python's descriptor + prototol. This class is the base class for all such descriptors. The + only magic we use is a custom metaclass for the main :class:`HasTraits` + class that does the following: + + 1. Sets the :attr:`name` attribute of every :class:`TraitType` + instance in the class dict to the name of the attribute. + 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 + accept superclasses for :class:`This` values. + """ + + metadata = {} + default_value = Undefined + allow_none = False + info_text = 'any value' + + def __init__(self, default_value=NoDefaultSpecified, allow_none=None, **metadata): + """Create a TraitType. + """ + if default_value is not NoDefaultSpecified: + self.default_value = default_value + if allow_none is not None: + self.allow_none = allow_none + + if 'default' in metadata: + # Warn the user that they probably meant default_value. + warn( + "Parameter 'default' passed to TraitType. " + "Did you mean 'default_value'?" + ) + + 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 + + self.init() + + def init(self): + pass + + def get_default_value(self): + """Create a new instance of the default value.""" + return self.default_value + + def instance_init(self): + """Part of the initialization which may depends on the underlying + HasTraits instance. + + It is typically overloaded for specific trait types. + + This method is called by :meth:`HasTraits.__new__` and in the + :meth:`TraitType.instance_init` method of trait types holding + other trait types. + """ + pass + + def init_default_value(self, obj): + """Instantiate the default value for the trait type. + + This method is called by :meth:`TraitType.set_default_value` in the + case a default value is provided at construction time or later when + accessing the trait value for the first time in + :meth:`HasTraits.__get__`. + """ + value = self.get_default_value() + value = self._validate(obj, value) + obj._trait_values[self.name] = value + return value + + def set_default_value(self, obj): + """Set the default value on a per instance basis. + + This method is called by :meth:`HasTraits.__new__` to instantiate and + validate the default value. The creation and validation of + default values must be delayed until the parent :class:`HasTraits` + class has been instantiated. + Parameters + ---------- + obj : :class:`HasTraits` instance + The parent :class:`HasTraits` instance that has just been + created. + """ + # 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. + self.init_default_value(obj) + return + # Complete the dynamic initialization. + obj._trait_dyn_inits[self.name] = meth_name + + def __get__(self, obj, cls=None): + """Get the value of the trait by self.name for the instance. + + Default values are instantiated when :meth:`HasTraits.__new__` + is called. Thus by the time this method gets called either the + default value or a user defined value (they called :meth:`__set__`) + is in the :class:`HasTraits` instance. + """ + if obj is None: + return self + else: + try: + value = obj._trait_values[self.name] + except KeyError: + # Check for a dynamic initializer. + if self.name in obj._trait_dyn_inits: + method = getattr(obj, obj._trait_dyn_inits[self.name]) + value = method() + # FIXME: Do we really validate here? + value = self._validate(obj, value) + obj._trait_values[self.name] = value + return value + else: + return self.init_default_value(obj) + except Exception: + # HasTraits should call set_default_value to populate + # this. So this should never be reached. + raise TraitError('Unexpected error in TraitType: ' + 'default value not set properly') + else: + return value + + def __set__(self, obj, value): + new_value = self._validate(obj, value) + try: + old_value = obj._trait_values[self.name] + except KeyError: + old_value = Undefined + + obj._trait_values[self.name] = new_value + try: + silent = bool(old_value == new_value) + except: + # if there is an error in comparing, default to notify + silent = False + if silent is not True: + # we explicitly compare silent to True just in case the equality + # comparison above returns something other than True/False + obj._notify_trait(self.name, old_value, new_value) + + def _validate(self, obj, value): + if value is None and self.allow_none: + return value + if hasattr(self, 'validate'): + value = self.validate(obj, value) + if obj._cross_validation_lock is False: + value = self._cross_validate(obj, value) + return value + + def _cross_validate(self, obj, value): + if hasattr(obj, '_%s_validate' % self.name): + cross_validate = getattr(obj, '_%s_validate' % self.name) + value = cross_validate(value, self) + return value + + def __or__(self, other): + if isinstance(other, Union): + return Union([self] + other.trait_types) + else: + return Union([self, other]) + + def info(self): + return self.info_text + + def error(self, obj, value): + if obj is not None: + e = "The '%s' trait 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' trait must be %s, but a value of %r was specified." \ + % (self.name, self.info(), repr_type(value)) + raise TraitError(e) + + def get_metadata(self, key, default=None): + return getattr(self, '_metadata', {}).get(key, default) + + def set_metadata(self, key, value): + getattr(self, '_metadata', {})[key] = value + + +#----------------------------------------------------------------------------- +# The HasTraits implementation +#----------------------------------------------------------------------------- + + +class MetaHasTraits(type): + """A metaclass for HasTraits. + + This metaclass makes sure that any TraitType class attributes are + instantiated and sets their name attribute. + """ + + def __new__(mcls, name, bases, classdict): + """Create the HasTraits class. + + This instantiates all TraitTypes in the class dict and sets their + :attr:`name` attribute. + """ + # print "MetaHasTraitlets (mcls, name): ", mcls, name + # print "MetaHasTraitlets (bases): ", bases + # print "MetaHasTraitlets (classdict): ", classdict + for k,v in iteritems(classdict): + if isinstance(v, TraitType): + v.name = k + elif inspect.isclass(v): + if issubclass(v, TraitType): + vinst = v() + vinst.name = k + classdict[k] = vinst + return super(MetaHasTraits, mcls).__new__(mcls, name, bases, classdict) + + def __init__(cls, name, bases, classdict): + """Finish initializing the HasTraits class. + + This sets the :attr:`this_class` attribute of each TraitType in the + class dict to the newly created class ``cls``. + """ + for k, v in iteritems(classdict): + if isinstance(v, TraitType): + v.this_class = cls + super(MetaHasTraits, cls).__init__(name, bases, classdict) + + +class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)): + + def __new__(cls, *args, **kw): + # This is needed because object.__new__ only accepts + # the cls argument. + new_meth = super(HasTraits, cls).__new__ + if new_meth is object.__new__: + inst = new_meth(cls) + else: + inst = new_meth(cls, **kw) + inst._trait_values = {} + inst._trait_notifiers = {} + inst._trait_dyn_inits = {} + inst._cross_validation_lock = True + # Here we tell all the TraitType instances to set their default + # values on the instance. + for key in dir(cls): + # 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: + if isinstance(value, TraitType): + value.instance_init() + if key not in kw: + value.set_default_value(inst) + inst._cross_validation_lock = False + return inst + + def __init__(self, *args, **kw): + # Allow trait values to be set using keyword arguments. + # We need to use setattr for this to trigger validation and + # notifications. + with self.hold_trait_notifications(): + for key, value in iteritems(kw): + setattr(self, key, value) + + @contextlib.contextmanager + def hold_trait_notifications(self): + """Context manager for bundling trait change notifications and cross + validation. + + Use this when doing multiple trait assignments (init, config), to avoid + race conditions in trait notifiers requesting other trait values. + All trait notifications will fire after all values have been assigned. + """ + if self._cross_validation_lock is True: + yield + return + else: + self._cross_validation_lock = True + cache = {} + notifications = {} + _notify_trait = self._notify_trait + + def cache_values(*a): + cache[a[0]] = a + + def hold_notifications(*a): + notifications[a[0]] = a + + self._notify_trait = cache_values + + try: + yield + finally: + try: + self._notify_trait = hold_notifications + for name in cache: + if hasattr(self, '_%s_validate' % name): + cross_validate = getattr(self, '_%s_validate' % name) + setattr(self, name, cross_validate(getattr(self, name), self)) + except TraitError as e: + self._notify_trait = lambda *x: None + for name in cache: + if cache[name][1] is not Undefined: + setattr(self, name, cache[name][1]) + else: + delattr(self, name) + cache = {} + notifications = {} + raise e + finally: + self._notify_trait = _notify_trait + self._cross_validation_lock = False + if isinstance(_notify_trait, types.MethodType): + # FIXME: remove when support is bumped to 3.4. + # when original method is restored, + # remove the redundant value from __dict__ + # (only used to preserve pickleability on Python < 3.4) + self.__dict__.pop('_notify_trait', None) + # trigger delayed notifications + for v in dict(cache, **notifications).values(): + self._notify_trait(*v) + + def _notify_trait(self, name, old_value, new_value): + + # First dynamic ones + callables = [] + callables.extend(self._trait_notifiers.get(name,[])) + callables.extend(self._trait_notifiers.get('anytrait',[])) + + # 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 = 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 TraitError('a trait changed callback ' + 'must have 0-3 arguments.') + else: + raise TraitError('a trait changed callback ' + 'must be callable.') + + + def _add_notifiers(self, handler, name): + if name not in self._trait_notifiers: + nlist = [] + self._trait_notifiers[name] = nlist + else: + nlist = self._trait_notifiers[name] + if handler not in nlist: + nlist.append(handler) + + def _remove_notifiers(self, handler, name): + if name in self._trait_notifiers: + nlist = self._trait_notifiers[name] + try: + index = nlist.index(handler) + except ValueError: + pass + else: + del nlist[index] + + def on_trait_change(self, handler, name=None, remove=False): + """Setup a handler to be called when a trait changes. + + This is used to setup dynamic notifications of trait changes. + + Static handlers can be created by creating methods on a HasTraits + subclass with the naming convention '_[traitname]_changed'. Thus, + to create static handler for the trait '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 trait 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 traits. 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) + + @classmethod + def class_trait_names(cls, **metadata): + """Get a list of all the names of this class' traits. + + This method is just like the :meth:`trait_names` method, + but is unbound. + """ + return cls.class_traits(**metadata).keys() + + @classmethod + def class_traits(cls, **metadata): + """Get a `dict` of all the traits of this class. The dictionary + is keyed on the name and the values are the TraitType objects. + + This method is just like the :meth:`traits` method, but is unbound. + + The TraitTypes returned don't know anything about the values + that the various HasTrait's instances are holding. + + The metadata kwargs allow functions to be passed in which + filter traits based on metadata values. The functions should + take a single value as an argument and return a boolean. If + any function returns False, then the trait is not included in + the output. This does not allow for any simple way of + testing that a metadata name exists and has any + value because get_metadata returns None if a metadata key + doesn't exist. + """ + traits = dict([memb for memb in getmembers(cls) if + isinstance(memb[1], TraitType)]) + + if len(metadata) == 0: + return traits + + for meta_name, meta_eval in metadata.items(): + if type(meta_eval) is not FunctionType: + metadata[meta_name] = _SimpleTest(meta_eval) + + result = {} + for name, trait in traits.items(): + for meta_name, meta_eval in metadata.items(): + if not meta_eval(trait.get_metadata(meta_name)): + break + else: + result[name] = trait + + return result + + def trait_names(self, **metadata): + """Get a list of all the names of this class' traits.""" + return self.traits(**metadata).keys() + + def traits(self, **metadata): + """Get a `dict` of all the traits of this class. The dictionary + is keyed on the name and the values are the TraitType objects. + + The TraitTypes returned don't know anything about the values + that the various HasTrait's instances are holding. + + The metadata kwargs allow functions to be passed in which + filter traits based on metadata values. The functions should + take a single value as an argument and return a boolean. If + any function returns False, then the trait is not included in + the output. This does not allow for any simple way of + testing that a metadata name exists and has any + value because get_metadata returns None if a metadata key + doesn't exist. + """ + traits = dict([memb for memb in getmembers(self.__class__) if + isinstance(memb[1], TraitType)]) + + if len(metadata) == 0: + return traits + + for meta_name, meta_eval in metadata.items(): + if type(meta_eval) is not FunctionType: + metadata[meta_name] = _SimpleTest(meta_eval) + + result = {} + for name, trait in traits.items(): + for meta_name, meta_eval in metadata.items(): + if not meta_eval(trait.get_metadata(meta_name)): + break + else: + result[name] = trait + + return result + + def trait_metadata(self, traitname, key, default=None): + """Get metadata values for trait by key.""" + try: + trait = getattr(self.__class__, traitname) + except AttributeError: + raise TraitError("Class %s does not have a trait named %s" % + (self.__class__.__name__, traitname)) + else: + return trait.get_metadata(key, default) + + def add_trait(self, traitname, trait): + """Dynamically add a trait attribute to the HasTraits instance.""" + self.__class__ = type(self.__class__.__name__, (self.__class__,), + {traitname: trait}) + trait.set_default_value(self) + +#----------------------------------------------------------------------------- +# Actual TraitTypes implementations/subclasses +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# TraitTypes subclasses for handling classes and instances of classes +#----------------------------------------------------------------------------- + + +class ClassBasedTraitType(TraitType): + """ + A trait with error reporting and string -> type resolution for Type, + Instance and This. + """ + + def _resolve_string(self, string): + """ + Resolve a string supplied for a type into an actual object. + """ + return import_item(string) + + def error(self, obj, value): + kind = type(value) + if (not py3compat.PY3) and kind is InstanceType: + msg = 'class %s' % value.__class__.__name__ + else: + msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) ) + + if obj is not None: + e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \ + % (self.name, class_of(obj), + self.info(), msg) + else: + e = "The '%s' trait must be %s, but a value of %r was specified." \ + % (self.name, self.info(), msg) + + raise TraitError(e) + + +class Type(ClassBasedTraitType): + """A trait whose value must be a subclass of a specified class.""" + + def __init__ (self, default_value=None, klass=None, allow_none=False, + **metadata): + """Construct a Type trait + + A Type trait specifies that its values must be subclasses of + a particular class. + + If only ``default_value`` is given, it is used for the ``klass`` as + well. + + Parameters + ---------- + 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 + :class:`HasTraits` class is instantiated. + klass : class, str, None + Values of this trait must be a subclass of klass. The klass + may be specified in a string like: 'foo.bar.MyClass'. + The string is resolved into real class, when the parent + :class:`HasTraits` class is instantiated. + allow_none : bool [ default True ] + 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 + + if not (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)): + raise TraitError("A Type trait must specify a class.") + + self.klass = klass + + super(Type, self).__init__(default_value, allow_none=allow_none, **metadata) + + def validate(self, obj, value): + """Validates that the value is a valid object instance.""" + if isinstance(value, py3compat.string_types): + try: + value = self._resolve_string(value) + except ImportError: + raise TraitError("The '%s' trait of %s instance must be a type, but " + "%r could not be imported" % (self.name, obj, value)) + try: + if issubclass(value, self.klass): + return value + except: + pass + + self.error(obj, value) + + def info(self): + """ Returns a description of the trait.""" + if isinstance(self.klass, py3compat.string_types): + klass = self.klass + else: + klass = self.klass.__name__ + result = 'a subclass of ' + klass + if self.allow_none: + return result + ' or None' + return result + + def instance_init(self): + self._resolve_classes() + super(Type, self).instance_init() + + def _resolve_classes(self): + if isinstance(self.klass, py3compat.string_types): + self.klass = self._resolve_string(self.klass) + if isinstance(self.default_value, py3compat.string_types): + self.default_value = self._resolve_string(self.default_value) + + def get_default_value(self): + return self.default_value + + +class DefaultValueGenerator(object): + """A class for generating new default value instances.""" + + def __init__(self, *args, **kw): + self.args = args + self.kw = kw + + def generate(self, klass): + return klass(*self.args, **self.kw) + + +class Instance(ClassBasedTraitType): + """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. + + Subclasses can declare default classes by overriding the klass attribute + """ + + klass = None + + def __init__(self, klass=None, args=None, kw=None, allow_none=False, + **metadata ): + """Construct an Instance trait. + + This trait allows values that are instances of a particular + class or its subclasses. 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. + + Parameters + ---------- + klass : class, str + The class that forms the basis for the trait. Class names + can also be specified as strings, like 'foo.bar.Bar'. + args : tuple + Positional arguments for generating the default value. + kw : dict + Keyword arguments for generating the default value. + allow_none : bool [default True] + Indicates whether None is allowed as a value. + + Notes + ----- + 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 exactly one of ``args`` or ``kw`` is + None, the None is replaced by ``()`` or ``{}``, respectively. + """ + if klass is None: + klass = self.klass + + if (klass is not None) and (inspect.isclass(klass) or isinstance(klass, py3compat.string_types)): + self.klass = klass + else: + raise TraitError('The klass attribute must be a class' + ' not: %r' % klass) + + # self.klass is a class, so handle default_value + if args is None and kw is None: + default_value = None + else: + if args is None: + # kw is not None + args = () + elif kw is None: + # args is not None + kw = {} + + if not isinstance(kw, dict): + raise TraitError("The 'kw' argument must be a dict or None.") + if not isinstance(args, tuple): + raise TraitError("The 'args' argument must be a tuple or None.") + + default_value = DefaultValueGenerator(*args, **kw) + + super(Instance, self).__init__(default_value, allow_none=allow_none, **metadata) + + def validate(self, obj, value): + if isinstance(value, self.klass): + return value + else: + self.error(obj, value) + + def info(self): + if isinstance(self.klass, py3compat.string_types): + klass = self.klass + else: + klass = self.klass.__name__ + result = class_of(klass) + if self.allow_none: + return result + ' or None' + + return result + + def instance_init(self): + self._resolve_classes() + super(Instance, self).instance_init() + + def _resolve_classes(self): + if isinstance(self.klass, py3compat.string_types): + self.klass = self._resolve_string(self.klass) + + def get_default_value(self): + """Instantiate a default value instance. + + This is called when the containing HasTraits classes' + :meth:`__new__` method is called to ensure that a unique instance + is created for each HasTraits instance. + """ + dv = self.default_value + if isinstance(dv, DefaultValueGenerator): + return dv.generate(self.klass) + else: + return dv + + +class ForwardDeclaredMixin(object): + """ + Mixin for forward-declared versions of Instance and Type. + """ + def _resolve_string(self, string): + """ + Find the specified class name by looking for it in the module in which + our this_class attribute was defined. + """ + modname = self.this_class.__module__ + return import_item('.'.join([modname, string])) + + +class ForwardDeclaredType(ForwardDeclaredMixin, Type): + """ + Forward-declared version of Type. + """ + pass + + +class ForwardDeclaredInstance(ForwardDeclaredMixin, Instance): + """ + Forward-declared version of Instance. + """ + pass + + +class This(ClassBasedTraitType): + """A trait for instances of the class containing this trait. + + Because how how and when class bodies are executed, the ``This`` + trait can only have a default value of None. This, and because we + always validate default values, ``allow_none`` is *always* true. + """ + + info_text = 'an instance of the same type as the receiver or None' + + def __init__(self, **metadata): + super(This, self).__init__(None, **metadata) + + def validate(self, obj, value): + # What if value is a superclass of obj.__class__? This is + # complicated if it was the superclass that defined the This + # trait. + if isinstance(value, self.this_class) or (value is None): + return value + else: + self.error(obj, value) + + +class Union(TraitType): + """A trait type representing a Union type.""" + + def __init__(self, trait_types, **metadata): + """Construct a Union trait. + + This trait allows values that are allowed by at least one of the + specified trait types. A Union traitlet cannot have metadata on + its own, besides the metadata of the listed types. + + Parameters + ---------- + trait_types: sequence + The list of trait types of length at least 1. + + Notes + ----- + Union([Float(), Bool(), Int()]) attempts to validate the provided values + with the validation function of Float, then Bool, and finally Int. + """ + self.trait_types = trait_types + self.info_text = " or ".join([tt.info_text for tt in self.trait_types]) + self.default_value = self.trait_types[0].get_default_value() + super(Union, self).__init__(**metadata) + + def instance_init(self): + for trait_type in self.trait_types: + trait_type.name = self.name + trait_type.this_class = self.this_class + trait_type.instance_init() + super(Union, self).instance_init() + + def validate(self, obj, value): + for trait_type in self.trait_types: + try: + v = trait_type._validate(obj, value) + self._metadata = trait_type._metadata + return v + except TraitError: + continue + self.error(obj, value) + + def __or__(self, other): + if isinstance(other, Union): + return Union(self.trait_types + other.trait_types) + else: + return Union(self.trait_types + [other]) + +#----------------------------------------------------------------------------- +# Basic TraitTypes implementations/subclasses +#----------------------------------------------------------------------------- + + +class Any(TraitType): + default_value = None + info_text = 'any value' + + +class Int(TraitType): + """An int trait.""" + + default_value = 0 + info_text = 'an int' + + def validate(self, obj, value): + if isinstance(value, int): + return value + self.error(obj, value) + +class CInt(Int): + """A casting version of the int trait.""" + + def validate(self, obj, value): + try: + return int(value) + except: + self.error(obj, value) + +if py3compat.PY3: + Long, CLong = Int, CInt + Integer = Int +else: + class Long(TraitType): + """A long integer trait.""" + + default_value = 0 + 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 CLong(Long): + """A casting version of the long integer trait.""" + + def validate(self, obj, value): + try: + return long(value) + except: + self.error(obj, value) + + class Integer(TraitType): + """An integer trait. + + Longs that are unnecessary (<= sys.maxint) are cast to ints.""" + + default_value = 0 + info_text = 'an integer' + + def validate(self, obj, value): + if isinstance(value, int): + return value + if isinstance(value, long): + # downcast longs that fit in int: + # note that int(n > sys.maxint) returns a long, so + # we don't need a condition on this cast + return int(value) + if sys.platform == "cli": + from System import Int64 + if isinstance(value, Int64): + return int(value) + self.error(obj, value) + + +class Float(TraitType): + """A float trait.""" + + 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 CFloat(Float): + """A casting version of the float trait.""" + + def validate(self, obj, value): + try: + return float(value) + except: + self.error(obj, value) + +class Complex(TraitType): + """A trait for complex numbers.""" + + 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 CComplex(Complex): + """A casting version of the complex number trait.""" + + def validate (self, obj, value): + try: + return complex(value) + except: + self.error(obj, value) + +# We should always be explicit about whether we're using bytes or unicode, both +# for Python 3 conversion and for reliable unicode behaviour on Python 2. So +# we don't have a Str type. +class Bytes(TraitType): + """A trait for byte strings.""" + + default_value = b'' + info_text = 'a bytes object' + + def validate(self, obj, value): + if isinstance(value, bytes): + return value + self.error(obj, value) + + +class CBytes(Bytes): + """A casting version of the byte string trait.""" + + def validate(self, obj, value): + try: + return bytes(value) + except: + self.error(obj, value) + + +class Unicode(TraitType): + """A trait for unicode strings.""" + + default_value = u'' + info_text = 'a unicode string' + + def validate(self, obj, value): + if isinstance(value, py3compat.unicode_type): + return value + if isinstance(value, bytes): + try: + return value.decode('ascii', 'strict') + except UnicodeDecodeError: + msg = "Could not decode {!r} for unicode trait '{}' of {} instance." + raise TraitError(msg.format(value, self.name, class_of(obj))) + self.error(obj, value) + + +class CUnicode(Unicode): + """A casting version of the unicode trait.""" + + def validate(self, obj, value): + try: + return py3compat.unicode_type(value) + except: + self.error(obj, value) + + +class ObjectName(TraitType): + """A string holding a valid object name in this version of Python. + + This does not check that the name exists in any scope.""" + info_text = "a valid object identifier in Python" + + if py3compat.PY3: + # Python 3: + coerce_str = staticmethod(lambda _,s: s) + + else: + # Python 2: + def coerce_str(self, obj, value): + "In Python 2, coerce ascii-only unicode to str" + if isinstance(value, unicode): + try: + return str(value) + except UnicodeEncodeError: + self.error(obj, value) + return value + + def validate(self, obj, value): + value = self.coerce_str(obj, value) + + if isinstance(value, string_types) and py3compat.isidentifier(value): + return value + self.error(obj, value) + +class DottedObjectName(ObjectName): + """A string holding a valid dotted object name in Python, such as A.b3._c""" + def validate(self, obj, value): + value = self.coerce_str(obj, value) + + if isinstance(value, string_types) and py3compat.isidentifier(value, dotted=True): + return value + self.error(obj, value) + + +class Bool(TraitType): + """A boolean (True, False) trait.""" + + default_value = False + info_text = 'a boolean' + + def validate(self, obj, value): + if isinstance(value, bool): + return value + self.error(obj, value) + + +class CBool(Bool): + """A casting version of the boolean trait.""" + + def validate(self, obj, value): + try: + return bool(value) + except: + self.error(obj, value) + + +class Enum(TraitType): + """An enum that whose value must be in a given sequence.""" + + def __init__(self, values, default_value=None, **metadata): + self.values = values + super(Enum, self).__init__(default_value, **metadata) + + def validate(self, obj, 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): + """An enum of strings that are caseless in validate.""" + + def validate(self, obj, value): + if not isinstance(value, py3compat.string_types): + self.error(obj, value) + + for v in self.values: + if v.lower() == value.lower(): + return v + self.error(obj, value) + +class Container(Instance): + """An instance of a container (list, set, etc.) + + To be subclassed by overriding klass. + """ + klass = None + _cast_types = () + _valid_defaults = SequenceTypes + _trait = None + + def __init__(self, trait=None, default_value=None, allow_none=False, + **metadata): + """Create a container trait type from a list, set, or tuple. + + The default value is created by doing ``List(default_value)``, + which creates a copy of the ``default_value``. + + ``trait`` can be specified, which restricts the type of elements + in the container to that TraitType. + + If only one arg is given and it is not a Trait, it is taken as + ``default_value``: + + ``c = List([1,2,3])`` + + Parameters + ---------- + + trait : TraitType [ optional ] + the type for restricting the contents of the Container. If unspecified, + types are not checked. + + default_value : SequenceType [ optional ] + The default value for the Trait. Must be list/tuple/set, and + will be cast to the container type. + + allow_none : bool [ default False ] + Whether to allow the value to be None + + **metadata : any + further keys for extensions to the Trait (e.g. config) + + """ + # allow List([values]): + if default_value is None and not is_trait(trait): + default_value = trait + trait = None + + if default_value is None: + args = () + elif isinstance(default_value, self._valid_defaults): + args = (default_value,) + else: + raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value)) + + if is_trait(trait): + self._trait = trait() if isinstance(trait, type) else trait + self._trait.name = 'element' + elif trait is not None: + raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait)) + + super(Container,self).__init__(klass=self.klass, args=args, + allow_none=allow_none, **metadata) + + def element_error(self, obj, element, validator): + e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \ + % (self.name, class_of(obj), validator.info(), repr_type(element)) + raise TraitError(e) + + def validate(self, obj, value): + if isinstance(value, self._cast_types): + value = self.klass(value) + value = super(Container, self).validate(obj, value) + if value is None: + return value + + value = self.validate_elements(obj, value) + + return value + + def validate_elements(self, obj, value): + validated = [] + if self._trait is None or isinstance(self._trait, Any): + return value + for v in value: + try: + v = self._trait._validate(obj, v) + except TraitError: + self.element_error(obj, v, self._trait) + else: + validated.append(v) + return self.klass(validated) + + def instance_init(self): + if isinstance(self._trait, TraitType): + self._trait.this_class = self.this_class + self._trait.instance_init() + super(Container, self).instance_init() + + +class List(Container): + """An instance of a Python list.""" + klass = list + _cast_types = (tuple,) + + def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, **metadata): + """Create a List trait type from a list, set, or tuple. + + The default value is created by doing ``List(default_value)``, + which creates a copy of the ``default_value``. + + ``trait`` can be specified, which restricts the type of elements + in the container to that TraitType. + + If only one arg is given and it is not a Trait, it is taken as + ``default_value``: + + ``c = List([1,2,3])`` + + Parameters + ---------- + + trait : TraitType [ optional ] + the type for restricting the contents of the Container. If unspecified, + types are not checked. + + default_value : SequenceType [ optional ] + The default value for the Trait. Must be list/tuple/set, and + will be cast to the container type. + + minlen : Int [ default 0 ] + The minimum length of the input list + + maxlen : Int [ default sys.maxsize ] + The maximum length of the input list + + allow_none : bool [ default False ] + Whether to allow the value to be None + + **metadata : any + further keys for extensions to the Trait (e.g. config) + + """ + self._minlen = minlen + self._maxlen = maxlen + super(List, self).__init__(trait=trait, default_value=default_value, + **metadata) + + def length_error(self, obj, value): + e = "The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified." \ + % (self.name, class_of(obj), self._minlen, self._maxlen, value) + raise TraitError(e) + + def validate_elements(self, obj, value): + length = len(value) + if length < self._minlen or length > self._maxlen: + self.length_error(obj, value) + + return super(List, self).validate_elements(obj, value) + + def validate(self, obj, value): + value = super(List, self).validate(obj, value) + value = self.validate_elements(obj, value) + return value + + +class Set(List): + """An instance of a Python set.""" + klass = set + _cast_types = (tuple, list) + + +class Tuple(Container): + """An instance of a Python tuple.""" + klass = tuple + _cast_types = (list,) + + def __init__(self, *traits, **metadata): + """Tuple(*traits, default_value=None, **medatata) + + Create a tuple from a list, set, or tuple. + + Create a fixed-type tuple with Traits: + + ``t = Tuple(Int, Str, CStr)`` + + would be length 3, with Int,Str,CStr for each element. + + If only one arg is given and it is not a Trait, it is taken as + default_value: + + ``t = Tuple((1,2,3))`` + + Otherwise, ``default_value`` *must* be specified by keyword. + + Parameters + ---------- + + *traits : TraitTypes [ optional ] + the types for restricting the contents of the Tuple. If unspecified, + types are not checked. If specified, then each positional argument + corresponds to an element of the tuple. Tuples defined with traits + are of fixed length. + + default_value : SequenceType [ optional ] + The default value for the Tuple. Must be list/tuple/set, and + will be cast to a tuple. If `traits` are specified, the + `default_value` must conform to the shape and type they specify. + + allow_none : bool [ default False ] + Whether to allow the value to be None + + **metadata : any + further keys for extensions to the Trait (e.g. config) + + """ + default_value = metadata.pop('default_value', None) + allow_none = metadata.pop('allow_none', True) + + # allow Tuple((values,)): + if len(traits) == 1 and default_value is None and not is_trait(traits[0]): + default_value = traits[0] + traits = () + + if default_value is None: + args = () + elif isinstance(default_value, self._valid_defaults): + args = (default_value,) + else: + raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value)) + + self._traits = [] + for trait in traits: + t = trait() if isinstance(trait, type) else trait + t.name = 'element' + self._traits.append(t) + + if self._traits and default_value is None: + # don't allow default to be an empty container if length is specified + args = None + super(Container,self).__init__(klass=self.klass, args=args, allow_none=allow_none, **metadata) + + def validate_elements(self, obj, value): + if not self._traits: + # nothing to validate + return value + if len(value) != len(self._traits): + e = "The '%s' trait of %s instance requires %i elements, but a value of %s was specified." \ + % (self.name, class_of(obj), len(self._traits), repr_type(value)) + raise TraitError(e) + + validated = [] + for t, v in zip(self._traits, value): + try: + v = t._validate(obj, v) + except TraitError: + self.element_error(obj, v, t) + else: + validated.append(v) + return tuple(validated) + + def instance_init(self): + for trait in self._traits: + if isinstance(trait, TraitType): + trait.this_class = self.this_class + trait.instance_init() + super(Container, self).instance_init() + + +class Dict(Instance): + """An instance of a Python dict.""" + _trait = None + + def __init__(self, trait=None, default_value=NoDefaultSpecified, allow_none=False, **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``. + + trait : TraitType [ optional ] + the type for restricting the contents of the Container. If unspecified, + types are not checked. + + default_value : SequenceType [ optional ] + The default value for the Dict. Must be dict, tuple, or None, and + will be cast to a dict if not None. If `trait` is specified, the + `default_value` must conform to the constraints it specifies. + + allow_none : bool [ default False ] + Whether to allow the value to be None + + """ + if default_value is NoDefaultSpecified and trait is not None: + if not is_trait(trait): + default_value = trait + trait = None + if default_value is NoDefaultSpecified: + default_value = {} + if default_value is None: + args = None + 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) + + if is_trait(trait): + self._trait = trait() if isinstance(trait, type) else trait + self._trait.name = 'element' + elif trait is not None: + raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait)) + + super(Dict,self).__init__(klass=dict, args=args, + allow_none=allow_none, **metadata) + + def element_error(self, obj, element, validator): + e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \ + % (self.name, class_of(obj), validator.info(), repr_type(element)) + raise TraitError(e) + + def validate(self, obj, value): + value = super(Dict, self).validate(obj, value) + if value is None: + return value + value = self.validate_elements(obj, value) + return value + + def validate_elements(self, obj, value): + if self._trait is None or isinstance(self._trait, Any): + return value + validated = {} + for key in value: + v = value[key] + try: + v = self._trait._validate(obj, v) + except TraitError: + self.element_error(obj, v, self._trait) + else: + validated[key] = v + return self.klass(validated) + + def instance_init(self): + if isinstance(self._trait, TraitType): + self._trait.this_class = self.this_class + self._trait.instance_init() + super(Dict, self).instance_init() + + +class EventfulDict(Instance): + """An instance of an EventfulDict.""" + + def __init__(self, default_value={}, allow_none=False, **metadata): + """Create a EventfulDict trait type from a dict. + + The default value is created by doing + ``eventful.EvenfulDict(default_value)``, which creates a copy of the + ``default_value``. + """ + if default_value is None: + args = None + elif isinstance(default_value, dict): + args = (default_value,) + elif isinstance(default_value, SequenceTypes): + args = (default_value,) + else: + raise TypeError('default value of EventfulDict was %s' % default_value) + + super(EventfulDict, self).__init__(klass=eventful.EventfulDict, args=args, + allow_none=allow_none, **metadata) + + +class EventfulList(Instance): + """An instance of an EventfulList.""" + + def __init__(self, default_value=None, allow_none=False, **metadata): + """Create a EventfulList trait type from a dict. + + The default value is created by doing + ``eventful.EvenfulList(default_value)``, which creates a copy of the + ``default_value``. + """ + if default_value is None: + args = ((),) + else: + args = (default_value,) + + super(EventfulList, self).__init__(klass=eventful.EventfulList, args=args, + allow_none=allow_none, **metadata) + + +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], py3compat.string_types) and isinstance(value[1], int): + port = value[1] + if port >= 0 and port <= 65535: + return value + self.error(obj, value) + +class CRegExp(TraitType): + """A casting compiled regular expression trait. + + Accepts both strings and compiled regular expressions. The resulting + attribute will be a compiled regular expression.""" + + info_text = 'a regular expression' + + def validate(self, obj, value): + try: + return re.compile(value) + except: + self.error(obj, value)